summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Action/Base.php63
-rw-r--r--app/Action/CommentCreation.php83
-rw-r--r--app/Action/TaskAssignCategoryColor.php2
-rw-r--r--app/Action/TaskAssignCategoryLabel.php4
-rw-r--r--app/Action/TaskAssignColorCategory.php2
-rw-r--r--app/Action/TaskAssignColorColumn.php85
-rw-r--r--app/Action/TaskAssignColorUser.php2
-rw-r--r--app/Action/TaskAssignCurrentUser.php8
-rw-r--r--app/Action/TaskAssignSpecificUser.php2
-rw-r--r--app/Action/TaskAssignUser.php4
-rw-r--r--app/Action/TaskClose.php18
-rw-r--r--app/Action/TaskCreation.php8
-rw-r--r--app/Action/TaskDuplicateAnotherProject.php4
-rw-r--r--app/Action/TaskLogMoveAnotherColumn.php84
-rw-r--r--app/Action/TaskMoveAnotherProject.php4
-rw-r--r--app/Action/TaskMoveColumnAssigned.php90
-rw-r--r--app/Action/TaskMoveColumnUnAssigned.php90
-rw-r--r--app/Action/TaskOpen.php4
-rw-r--r--app/Action/TaskUpdateStartDate.php83
-rw-r--r--app/Api/Action.php98
-rw-r--r--app/Api/App.php22
-rw-r--r--app/Api/Base.php33
-rw-r--r--app/Api/Board.php52
-rw-r--r--app/Api/Category.php49
-rw-r--r--app/Api/Comment.php51
-rw-r--r--app/Api/File.php48
-rw-r--r--app/Api/Link.php111
-rw-r--r--app/Api/Project.php85
-rw-r--r--app/Api/ProjectPermission.php27
-rw-r--r--app/Api/Subtask.php64
-rw-r--r--app/Api/Swimlane.php77
-rw-r--r--app/Api/Task.php113
-rw-r--r--app/Api/TaskLink.php77
-rw-r--r--app/Api/User.php88
-rw-r--r--app/Auth/Base.php37
-rw-r--r--app/Auth/Database.php27
-rw-r--r--app/Auth/GitHub.php20
-rw-r--r--app/Auth/Google.php20
-rw-r--r--app/Auth/Ldap.php107
-rw-r--r--app/Auth/RememberMe.php31
-rw-r--r--app/Auth/ReverseProxy.php18
-rw-r--r--app/Console/Base.php58
-rw-r--r--app/Console/ProjectDailySummaryCalculation.php27
-rw-r--r--app/Console/ProjectDailySummaryExport.php34
-rw-r--r--app/Console/SubtaskExport.php34
-rw-r--r--app/Console/TaskExport.php34
-rw-r--r--app/Console/TaskOverdueNotification.php50
-rw-r--r--app/Console/TransitionExport.php34
-rw-r--r--app/Controller/Action.php30
-rw-r--r--app/Controller/Analytic.php170
-rw-r--r--app/Controller/App.php117
-rw-r--r--app/Controller/Auth.php67
-rw-r--r--app/Controller/Base.php263
-rw-r--r--app/Controller/Board.php501
-rw-r--r--app/Controller/Budget.php135
-rw-r--r--app/Controller/Calendar.php128
-rw-r--r--app/Controller/Category.php56
-rw-r--r--app/Controller/Column.php170
-rw-r--r--app/Controller/Comment.php76
-rw-r--r--app/Controller/Config.php84
-rw-r--r--app/Controller/Currency.php89
-rw-r--r--app/Controller/Export.php85
-rw-r--r--app/Controller/File.php79
-rw-r--r--app/Controller/Hourlyrate.php89
-rw-r--r--app/Controller/Ical.php99
-rw-r--r--app/Controller/Link.php162
-rw-r--r--app/Controller/Project.php333
-rw-r--r--app/Controller/Projectinfo.php97
-rw-r--r--app/Controller/Subtask.php184
-rw-r--r--app/Controller/Swimlane.php256
-rw-r--r--app/Controller/Task.php336
-rw-r--r--app/Controller/Tasklink.php179
-rw-r--r--app/Controller/Timetable.php39
-rw-r--r--app/Controller/Timetableday.php88
-rw-r--r--app/Controller/Timetableextra.php16
-rw-r--r--app/Controller/Timetableoff.php107
-rw-r--r--app/Controller/Timetableweek.php99
-rw-r--r--app/Controller/Twofactor.php167
-rw-r--r--app/Controller/User.php243
-rw-r--r--app/Controller/Webhook.php88
-rw-r--r--app/Core/Base.php112
-rw-r--r--app/Core/Cache.php58
-rw-r--r--app/Core/Cli.php75
-rw-r--r--app/Core/EmailClient.php46
-rw-r--r--app/Core/Event.php164
-rw-r--r--app/Core/Helper.php60
-rw-r--r--app/Core/HttpClient.php117
-rw-r--r--app/Core/Listener.php21
-rw-r--r--app/Core/Loader.php62
-rw-r--r--app/Core/Markdown.php47
-rw-r--r--app/Core/MemoryCache.php32
-rw-r--r--app/Core/Paginator.php461
-rw-r--r--app/Core/Registry.php83
-rw-r--r--app/Core/Request.php25
-rw-r--r--app/Core/Response.php38
-rw-r--r--app/Core/Router.php24
-rw-r--r--app/Core/Session.php64
-rw-r--r--app/Core/Template.php14
-rw-r--r--app/Core/Tool.php31
-rw-r--r--app/Core/Translator.php34
-rw-r--r--app/Event/AuthEvent.php27
-rw-r--r--app/Event/Base.php79
-rw-r--r--app/Event/CommentEvent.php7
-rw-r--r--app/Event/FileEvent.php7
-rw-r--r--app/Event/GenericEvent.php45
-rw-r--r--app/Event/NotificationListener.php83
-rw-r--r--app/Event/ProjectActivityListener.php61
-rw-r--r--app/Event/ProjectModificationDateListener.php30
-rw-r--r--app/Event/SubtaskEvent.php7
-rw-r--r--app/Event/TaskEvent.php7
-rw-r--r--app/Event/WebhookListener.php44
-rw-r--r--app/Helper/App.php56
-rw-r--r--app/Helper/Asset.php51
-rw-r--r--app/Helper/Datetime.php61
-rw-r--r--app/Helper/File.php56
-rw-r--r--app/Helper/Form.php323
-rw-r--r--app/Helper/Subtask.php42
-rw-r--r--app/Helper/Task.php59
-rw-r--r--app/Helper/Text.php91
-rw-r--r--app/Helper/Url.php117
-rw-r--r--app/Helper/User.php104
-rw-r--r--app/Integration/BitbucketWebhook.php97
-rw-r--r--app/Integration/GithubWebhook.php (renamed from app/Model/GithubWebhook.php)183
-rw-r--r--app/Integration/GitlabWebhook.php213
-rw-r--r--app/Integration/HipchatWebhook.php95
-rw-r--r--app/Integration/Jabber.php130
-rw-r--r--app/Integration/Mailgun.php97
-rw-r--r--app/Integration/Postmark.php97
-rw-r--r--app/Integration/SendgridWebhook.php74
-rw-r--r--app/Integration/SlackWebhook.php75
-rw-r--r--app/Integration/Smtp.php71
-rw-r--r--app/Library/password.php227
-rw-r--r--app/Locale/da_DK/translations.php (renamed from app/Locales/da_DK/translations.php)425
-rw-r--r--app/Locale/de_DE/translations.php (renamed from app/Locales/de_DE/translations.php)527
-rw-r--r--app/Locale/es_ES/translations.php (renamed from app/Locales/es_ES/translations.php)533
-rw-r--r--app/Locale/fi_FI/translations.php925
-rw-r--r--app/Locale/fr_FR/translations.php (renamed from app/Locales/fr_FR/translations.php)439
-rw-r--r--app/Locale/hu_HU/translations.php925
-rw-r--r--app/Locale/it_IT/translations.php925
-rw-r--r--app/Locale/ja_JP/translations.php (renamed from app/Locales/ja_JP/translations.php)443
-rw-r--r--app/Locale/nl_NL/translations.php925
-rw-r--r--app/Locale/pl_PL/translations.php925
-rw-r--r--app/Locale/pt_BR/translations.php925
-rw-r--r--app/Locale/ru_RU/translations.php925
-rw-r--r--app/Locale/sr_Latn_RS/translations.php925
-rw-r--r--app/Locale/sv_SE/translations.php (renamed from app/Locales/sv_SE/translations.php)447
-rw-r--r--app/Locale/th_TH/translations.php925
-rw-r--r--app/Locale/tr_TR/translations.php925
-rw-r--r--app/Locale/zh_CN/translations.php (renamed from app/Locales/zh_CN/translations.php)459
-rw-r--r--app/Locales/fi_FI/translations.php554
-rw-r--r--app/Locales/it_IT/translations.php554
-rw-r--r--app/Locales/pl_PL/translations.php554
-rw-r--r--app/Locales/pt_BR/translations.php554
-rw-r--r--app/Locales/ru_RU/translations.php554
-rw-r--r--app/Model/Acl.php180
-rw-r--r--app/Model/Action.php43
-rw-r--r--app/Model/Authentication.php27
-rw-r--r--app/Model/Base.php149
-rw-r--r--app/Model/Board.php203
-rw-r--r--app/Model/Budget.php214
-rw-r--r--app/Model/Category.php107
-rw-r--r--app/Model/Color.php116
-rw-r--r--app/Model/Comment.php22
-rw-r--r--app/Model/Config.php133
-rw-r--r--app/Model/Currency.php106
-rw-r--r--app/Model/DateParser.php65
-rw-r--r--app/Model/File.php279
-rw-r--r--app/Model/HourlyRate.php121
-rw-r--r--app/Model/LastLogin.php2
-rw-r--r--app/Model/Link.php223
-rw-r--r--app/Model/Notification.php365
-rw-r--r--app/Model/Project.php209
-rw-r--r--app/Model/ProjectActivity.php162
-rw-r--r--app/Model/ProjectAnalytic.php90
-rw-r--r--app/Model/ProjectDailySummary.php192
-rw-r--r--app/Model/ProjectDuplication.php108
-rw-r--r--app/Model/ProjectIntegration.php66
-rw-r--r--app/Model/ProjectPermission.php251
-rw-r--r--app/Model/SubTask.php277
-rw-r--r--app/Model/Subtask.php497
-rw-r--r--app/Model/SubtaskExport.php119
-rw-r--r--app/Model/SubtaskForecast.php124
-rw-r--r--app/Model/SubtaskTimeTracking.php340
-rw-r--r--app/Model/Swimlane.php561
-rw-r--r--app/Model/Task.php444
-rw-r--r--app/Model/TaskCreation.php82
-rwxr-xr-xapp/Model/TaskDuplication.php248
-rw-r--r--app/Model/TaskExport.php19
-rw-r--r--app/Model/TaskFilter.php467
-rw-r--r--app/Model/TaskFinder.php216
-rw-r--r--app/Model/TaskLink.php273
-rw-r--r--app/Model/TaskModification.php74
-rw-r--r--app/Model/TaskPermission.php4
-rw-r--r--app/Model/TaskPosition.php185
-rw-r--r--app/Model/TaskStatus.php131
-rw-r--r--app/Model/TaskValidator.php31
-rw-r--r--app/Model/TimeTracking.php45
-rw-r--r--app/Model/Timetable.php356
-rw-r--r--app/Model/TimetableDay.php87
-rw-r--r--app/Model/TimetableExtra.php22
-rw-r--r--app/Model/TimetableOff.php125
-rw-r--r--app/Model/TimetableWeek.php91
-rw-r--r--app/Model/Transition.php170
-rw-r--r--app/Model/User.php196
-rw-r--r--app/Model/UserSession.php131
-rw-r--r--app/Model/Webhook.php141
-rw-r--r--app/Schema/Mysql.php481
-rw-r--r--app/Schema/Postgres.php463
-rw-r--r--app/Schema/Sqlite.php462
-rw-r--r--app/ServiceProvider/ClassProvider.php107
-rw-r--r--app/ServiceProvider/DatabaseProvider.php108
-rw-r--r--app/ServiceProvider/EventDispatcherProvider.php40
-rw-r--r--app/ServiceProvider/LoggingProvider.php28
-rw-r--r--app/Subscriber/AuthSubscriber.php27
-rw-r--r--app/Subscriber/BootstrapSubscriber.php23
-rw-r--r--app/Subscriber/NotificationSubscriber.php61
-rw-r--r--app/Subscriber/ProjectActivitySubscriber.php73
-rw-r--r--app/Subscriber/ProjectDailySummarySubscriber.php28
-rw-r--r--app/Subscriber/ProjectModificationDateSubscriber.php31
-rw-r--r--app/Subscriber/RecurringTaskSubscriber.php38
-rw-r--r--app/Subscriber/SubtaskTimesheetSubscriber.php47
-rw-r--r--app/Subscriber/TaskMovedDateSubscriber.php24
-rw-r--r--app/Subscriber/TransitionSubscriber.php26
-rw-r--r--app/Subscriber/WebhookSubscriber.php45
-rw-r--r--app/Template/action/event.php25
-rw-r--r--app/Template/action/index.php64
-rw-r--r--app/Template/action/params.php43
-rw-r--r--app/Template/action/remove.php15
-rw-r--r--app/Template/analytic/burndown.php34
-rw-r--r--app/Template/analytic/cfd.php32
-rw-r--r--app/Template/analytic/layout.php35
-rw-r--r--app/Template/analytic/sidebar.php17
-rw-r--r--app/Template/analytic/tasks.php34
-rw-r--r--app/Template/analytic/users.php34
-rw-r--r--app/Template/app/dashboard.php60
-rw-r--r--app/Template/app/forbidden.php (renamed from app/Templates/app_forbidden.php)4
-rw-r--r--app/Template/app/notfound.php5
-rw-r--r--app/Template/app/projects.php41
-rw-r--r--app/Template/app/subtasks.php41
-rw-r--r--app/Template/app/tasks.php41
-rw-r--r--app/Template/auth/index.php32
-rw-r--r--app/Template/board/assignee.php21
-rw-r--r--app/Template/board/category.php22
-rw-r--r--app/Template/board/comments.php13
-rw-r--r--app/Template/board/description.php5
-rw-r--r--app/Template/board/files.php31
-rw-r--r--app/Template/board/filters.php43
-rw-r--r--app/Template/board/index.php18
-rw-r--r--app/Template/board/public.php13
-rw-r--r--app/Template/board/show.php32
-rw-r--r--app/Template/board/subtasks.php7
-rw-r--r--app/Template/board/swimlane.php85
-rw-r--r--app/Template/board/task_footer.php61
-rw-r--r--app/Template/board/task_menu.php16
-rw-r--r--app/Template/board/task_private.php50
-rw-r--r--app/Template/board/task_public.php31
-rw-r--r--app/Template/board/tasklinks.php18
-rw-r--r--app/Template/budget/breakdown.php30
-rw-r--r--app/Template/budget/create.php47
-rw-r--r--app/Template/budget/index.php33
-rw-r--r--app/Template/budget/remove.php13
-rw-r--r--app/Template/budget/sidebar.php14
-rw-r--r--app/Template/calendar/show.php46
-rw-r--r--app/Template/calendar/sidebar.php40
-rw-r--r--app/Template/category/edit.php38
-rw-r--r--app/Template/category/index.php42
-rw-r--r--app/Template/category/remove.php (renamed from app/Templates/category_remove.php)5
-rw-r--r--app/Template/column/edit.php42
-rw-r--r--app/Template/column/index.php89
-rw-r--r--app/Template/column/remove.php (renamed from app/Templates/board_remove.php)4
-rw-r--r--app/Template/comment/create.php40
-rw-r--r--app/Template/comment/edit.php36
-rw-r--r--app/Template/comment/forbidden.php7
-rw-r--r--app/Template/comment/remove.php17
-rw-r--r--app/Template/comment/show.php52
-rw-r--r--app/Template/config/about.php57
-rw-r--r--app/Template/config/api.php18
-rw-r--r--app/Template/config/application.php30
-rw-r--r--app/Template/config/board.php25
-rw-r--r--app/Template/config/calendar.php33
-rw-r--r--app/Template/config/integrations.php87
-rw-r--r--app/Template/config/layout.php10
-rw-r--r--app/Template/config/project.php24
-rw-r--r--app/Template/config/sidebar.php35
-rw-r--r--app/Template/config/webhook.php35
-rw-r--r--app/Template/currency/index.php56
-rw-r--r--app/Template/event/comment_create.php12
-rw-r--r--app/Template/event/comment_update.php11
-rw-r--r--app/Template/event/events.php (renamed from app/Templates/project_events.php)6
-rw-r--r--app/Template/event/subtask_create.php (renamed from app/Templates/event_subtask_create.php)11
-rw-r--r--app/Template/event/subtask_update.php (renamed from app/Templates/event_subtask_update.php)11
-rw-r--r--app/Template/event/task_assignee_change.php18
-rw-r--r--app/Template/event/task_close.php11
-rw-r--r--app/Template/event/task_create.php11
-rw-r--r--app/Template/event/task_move_column.php12
-rw-r--r--app/Template/event/task_move_position.php13
-rw-r--r--app/Template/event/task_open.php11
-rw-r--r--app/Template/event/task_update.php11
-rw-r--r--app/Template/export/sidebar.php17
-rw-r--r--app/Template/export/subtasks.php26
-rw-r--r--app/Template/export/summary.php26
-rw-r--r--app/Template/export/tasks.php26
-rw-r--r--app/Template/export/transitions.php26
-rw-r--r--app/Template/file/new.php14
-rw-r--r--app/Template/file/open.php6
-rw-r--r--app/Template/file/remove.php15
-rw-r--r--app/Template/file/screenshot.php17
-rw-r--r--app/Template/file/show.php56
-rw-r--r--app/Template/hourlyrate/index.php46
-rw-r--r--app/Template/hourlyrate/remove.php13
-rw-r--r--app/Template/layout.php71
-rw-r--r--app/Template/link/create.php18
-rw-r--r--app/Template/link/edit.php21
-rw-r--r--app/Template/link/index.php33
-rw-r--r--app/Template/link/remove.php15
-rw-r--r--app/Template/notification/comment_create.php7
-rw-r--r--app/Template/notification/comment_update.php7
-rw-r--r--app/Template/notification/file_create.php5
-rw-r--r--app/Template/notification/footer.php6
-rw-r--r--app/Template/notification/subtask_create.php17
-rw-r--r--app/Template/notification/subtask_update.php21
-rw-r--r--app/Template/notification/task_assignee_change.php (renamed from app/Templates/notification_task_assignee_change.php)6
-rw-r--r--app/Template/notification/task_close.php5
-rw-r--r--app/Template/notification/task_create.php (renamed from app/Templates/notification_task_update.php)18
-rw-r--r--app/Template/notification/task_move_column.php11
-rw-r--r--app/Template/notification/task_move_position.php11
-rw-r--r--app/Template/notification/task_open.php5
-rw-r--r--app/Template/notification/task_overdue.php18
-rw-r--r--app/Template/notification/task_update.php (renamed from app/Templates/notification_task_creation.php)12
-rw-r--r--app/Template/project/disable.php (renamed from app/Templates/project_disable.php)4
-rw-r--r--app/Template/project/dropdown.php41
-rw-r--r--app/Template/project/duplicate.php23
-rw-r--r--app/Template/project/edit.php44
-rw-r--r--app/Template/project/enable.php (renamed from app/Templates/project_enable.php)4
-rw-r--r--app/Template/project/feed.php (renamed from app/Templates/project_feed.php)12
-rw-r--r--app/Template/project/index.php67
-rw-r--r--app/Template/project/integrations.php95
-rw-r--r--app/Template/project/layout.php32
-rw-r--r--app/Template/project/new.php24
-rw-r--r--app/Template/project/remove.php (renamed from app/Templates/project_remove.php)4
-rw-r--r--app/Template/project/share.php19
-rw-r--r--app/Template/project/show.php73
-rw-r--r--app/Template/project/sidebar.php52
-rw-r--r--app/Template/project/users.php81
-rw-r--r--app/Template/projectinfo/activity.php30
-rw-r--r--app/Template/projectinfo/search.php43
-rw-r--r--app/Template/projectinfo/tasks.php33
-rw-r--r--app/Template/subtask/create.php27
-rw-r--r--app/Template/subtask/edit.php29
-rw-r--r--app/Template/subtask/icons.php7
-rw-r--r--app/Template/subtask/remove.php17
-rw-r--r--app/Template/subtask/restriction_change_status.php19
-rw-r--r--app/Template/subtask/show.php80
-rw-r--r--app/Template/swimlane/edit.php18
-rw-r--r--app/Template/swimlane/index.php47
-rw-r--r--app/Template/swimlane/remove.php17
-rw-r--r--app/Template/swimlane/table.php44
-rw-r--r--app/Template/task/activity.php5
-rw-r--r--app/Template/task/close.php15
-rw-r--r--app/Template/task/comments.php (renamed from app/Templates/task_comments.php)6
-rw-r--r--app/Template/task/details.php (renamed from app/Templates/task_details.php)29
-rw-r--r--app/Template/task/duplicate.php15
-rw-r--r--app/Template/task/duplicate_project.php24
-rw-r--r--app/Template/task/edit.php68
-rw-r--r--app/Template/task/edit_description.php38
-rw-r--r--app/Template/task/edit_recurrence.php52
-rw-r--r--app/Template/task/layout.php28
-rw-r--r--app/Template/task/move_project.php24
-rw-r--r--app/Template/task/new.php84
-rw-r--r--app/Template/task/open.php15
-rw-r--r--app/Template/task/public.php34
-rw-r--r--app/Template/task/recurring_info.php37
-rw-r--r--app/Template/task/remove.php15
-rw-r--r--app/Template/task/show.php15
-rw-r--r--app/Template/task/show_description.php (renamed from app/Templates/task_show_description.php)13
-rw-r--r--app/Template/task/sidebar.php67
-rw-r--r--app/Template/task/table.php56
-rw-r--r--app/Template/task/time.php15
-rw-r--r--app/Template/task/time_tracking.php27
-rw-r--r--app/Template/task/timesheet.php13
-rw-r--r--app/Template/task/transitions.php26
-rw-r--r--app/Template/tasklink/create.php37
-rw-r--r--app/Template/tasklink/edit.php34
-rw-r--r--app/Template/tasklink/remove.php15
-rw-r--r--app/Template/tasklink/show.php104
-rw-r--r--app/Template/timetable/index.php44
-rw-r--r--app/Template/timetable_day/index.php45
-rw-r--r--app/Template/timetable_day/remove.php13
-rw-r--r--app/Template/timetable_extra/index.php56
-rw-r--r--app/Template/timetable_extra/remove.php13
-rw-r--r--app/Template/timetable_off/index.php56
-rw-r--r--app/Template/timetable_off/remove.php13
-rw-r--r--app/Template/timetable_week/index.php46
-rw-r--r--app/Template/timetable_week/remove.php13
-rw-r--r--app/Template/twofactor/check.php10
-rw-r--r--app/Template/twofactor/disable.php14
-rw-r--r--app/Template/twofactor/index.php37
-rw-r--r--app/Template/user/calendar.php6
-rw-r--r--app/Template/user/edit.php42
-rw-r--r--app/Template/user/external.php (renamed from app/Templates/user_external.php)12
-rw-r--r--app/Template/user/index.php76
-rw-r--r--app/Template/user/last.php (renamed from app/Templates/user_last.php)6
-rw-r--r--app/Template/user/layout.php18
-rw-r--r--app/Template/user/new.php45
-rw-r--r--app/Template/user/notifications.php38
-rw-r--r--app/Template/user/password.php26
-rw-r--r--app/Template/user/remove.php13
-rw-r--r--app/Template/user/sessions.php (renamed from app/Templates/user_sessions.php)6
-rw-r--r--app/Template/user/share.php17
-rw-r--r--app/Template/user/show.php39
-rw-r--r--app/Template/user/sidebar.php77
-rw-r--r--app/Template/user/timesheet.php29
-rw-r--r--app/Templates/action_event.php22
-rw-r--r--app/Templates/action_index.php65
-rw-r--r--app/Templates/action_params.php40
-rw-r--r--app/Templates/action_remove.php14
-rw-r--r--app/Templates/app_index.php45
-rw-r--r--app/Templates/app_notfound.php9
-rw-r--r--app/Templates/board_assignee.php24
-rw-r--r--app/Templates/board_category.php24
-rw-r--r--app/Templates/board_edit.php58
-rw-r--r--app/Templates/board_index.php38
-rw-r--r--app/Templates/board_public.php34
-rw-r--r--app/Templates/board_show.php49
-rw-r--r--app/Templates/board_task.php109
-rw-r--r--app/Templates/category_edit.php16
-rw-r--r--app/Templates/category_index.php41
-rw-r--r--app/Templates/comment_create.php19
-rw-r--r--app/Templates/comment_edit.php17
-rw-r--r--app/Templates/comment_forbidden.php9
-rw-r--r--app/Templates/comment_remove.php16
-rw-r--r--app/Templates/comment_show.php41
-rw-r--r--app/Templates/config_about.php41
-rw-r--r--app/Templates/config_api.php18
-rw-r--r--app/Templates/config_application.php27
-rw-r--r--app/Templates/config_board.php29
-rw-r--r--app/Templates/config_layout.php13
-rw-r--r--app/Templates/config_sidebar.php22
-rw-r--r--app/Templates/config_webhook.php38
-rw-r--r--app/Templates/event_comment_create.php7
-rw-r--r--app/Templates/event_comment_update.php7
-rw-r--r--app/Templates/event_task_assignee_change.php12
-rw-r--r--app/Templates/event_task_close.php6
-rw-r--r--app/Templates/event_task_create.php6
-rw-r--r--app/Templates/event_task_move_column.php6
-rw-r--r--app/Templates/event_task_move_position.php6
-rw-r--r--app/Templates/event_task_open.php6
-rw-r--r--app/Templates/event_task_update.php6
-rw-r--r--app/Templates/file_new.php14
-rw-r--r--app/Templates/file_open.php6
-rw-r--r--app/Templates/file_remove.php14
-rw-r--r--app/Templates/file_show.php23
-rw-r--r--app/Templates/layout.php84
-rw-r--r--app/Templates/notification_comment_creation.php7
-rw-r--r--app/Templates/notification_comment_update.php7
-rw-r--r--app/Templates/notification_file_creation.php5
-rw-r--r--app/Templates/notification_footer.php6
-rw-r--r--app/Templates/notification_subtask_creation.php17
-rw-r--r--app/Templates/notification_subtask_update.php21
-rw-r--r--app/Templates/notification_task_close.php5
-rw-r--r--app/Templates/notification_task_due.php15
-rw-r--r--app/Templates/notification_task_move_column.php11
-rw-r--r--app/Templates/notification_task_move_position.php11
-rw-r--r--app/Templates/notification_task_open.php5
-rw-r--r--app/Templates/project_activity.php18
-rw-r--r--app/Templates/project_duplicate.php14
-rw-r--r--app/Templates/project_edit.php15
-rw-r--r--app/Templates/project_export.php24
-rw-r--r--app/Templates/project_index.php49
-rw-r--r--app/Templates/project_layout.php17
-rw-r--r--app/Templates/project_new.php22
-rw-r--r--app/Templates/project_search.php37
-rw-r--r--app/Templates/project_share.php19
-rw-r--r--app/Templates/project_show.php55
-rw-r--r--app/Templates/project_sidebar.php49
-rw-r--r--app/Templates/project_tasks.php23
-rw-r--r--app/Templates/project_users.php57
-rw-r--r--app/Templates/subtask_create.php27
-rw-r--r--app/Templates/subtask_edit.php32
-rw-r--r--app/Templates/subtask_remove.php16
-rw-r--r--app/Templates/subtask_show.php72
-rw-r--r--app/Templates/task_close.php14
-rw-r--r--app/Templates/task_duplicate.php14
-rw-r--r--app/Templates/task_duplicate_project.php24
-rw-r--r--app/Templates/task_edit.php51
-rw-r--r--app/Templates/task_edit_description.php22
-rw-r--r--app/Templates/task_layout.php16
-rw-r--r--app/Templates/task_move_project.php24
-rw-r--r--app/Templates/task_new.php55
-rw-r--r--app/Templates/task_open.php14
-rw-r--r--app/Templates/task_public.php27
-rw-r--r--app/Templates/task_remove.php14
-rw-r--r--app/Templates/task_show.php7
-rw-r--r--app/Templates/task_sidebar.php26
-rw-r--r--app/Templates/task_table.php56
-rw-r--r--app/Templates/task_time.php15
-rw-r--r--app/Templates/task_timesheet.php13
-rw-r--r--app/Templates/user_edit.php30
-rw-r--r--app/Templates/user_index.php71
-rw-r--r--app/Templates/user_layout.php19
-rw-r--r--app/Templates/user_login.php42
-rw-r--r--app/Templates/user_new.php39
-rw-r--r--app/Templates/user_notifications.php22
-rw-r--r--app/Templates/user_password.php23
-rw-r--r--app/Templates/user_remove.php12
-rw-r--r--app/Templates/user_show.php12
-rw-r--r--app/Templates/user_sidebar.php42
-rw-r--r--app/check_setup.php5
-rw-r--r--app/common.php33
-rw-r--r--app/constants.php26
-rw-r--r--app/functions.php163
-rw-r--r--app/helpers.php662
512 files changed, 36031 insertions, 10143 deletions
diff --git a/app/Action/Base.php b/app/Action/Base.php
index 80930a4c..f29e9323 100644
--- a/app/Action/Base.php
+++ b/app/Action/Base.php
@@ -2,23 +2,26 @@
namespace Action;
-use Core\Listener;
-use Core\Registry;
-use Core\Tool;
+use Event\GenericEvent;
+use Pimple\Container;
/**
* Base class for automatic actions
*
* @package action
* @author Frederic Guillot
- *
- * @property \Model\Acl $acl
- * @property \Model\Task $task
- * @property \Model\TaskFinder $taskFinder
*/
-abstract class Base implements Listener
+abstract class Base extends \Core\Base
{
/**
+ * Flag for called listener
+ *
+ * @access private
+ * @var boolean
+ */
+ private $called = false;
+
+ /**
* Project id
*
* @access private
@@ -43,12 +46,12 @@ abstract class Base implements Listener
protected $event_name = '';
/**
- * Registry instance
+ * Container instance
*
* @access protected
- * @var \Core\Registry
+ * @var \Pimple\Container
*/
- protected $registry;
+ protected $container;
/**
* Execute the action
@@ -100,15 +103,16 @@ abstract class Base implements Listener
* Constructor
*
* @access public
- * @param \Core\Registry $registry Regsitry instance
- * @param integer $project_id Project id
- * @param string $event_name Attached event name
+ * @param \Pimple\Container $container Container
+ * @param integer $project_id Project id
+ * @param string $event_name Attached event name
*/
- public function __construct(Registry $registry, $project_id, $event_name)
+ public function __construct(Container $container, $project_id, $event_name)
{
- $this->registry = $registry;
+ $this->container = $container;
$this->project_id = $project_id;
$this->event_name = $event_name;
+ $this->called = false;
}
/**
@@ -123,18 +127,6 @@ abstract class Base implements Listener
}
/**
- * Load automatically models
- *
- * @access public
- * @param string $name Model name
- * @return mixed
- */
- public function __get($name)
- {
- return Tool::loadModel($this->registry, $name);
- }
-
- /**
* Set an user defined parameter
*
* @access public
@@ -178,7 +170,6 @@ abstract class Base implements Listener
* Check if the event is compatible with the action
*
* @access public
- * @param array $data Event data dictionary
* @return bool
*/
public function hasCompatibleEvent()
@@ -220,12 +211,20 @@ abstract class Base implements Listener
* Execute the action
*
* @access public
- * @param array $data Event data dictionary
- * @return bool True if the action was executed or false when not executed
+ * @param \Event\GenericEvent $event Event data dictionary
+ * @return bool True if the action was executed or false when not executed
*/
- public function execute(array $data)
+ public function execute(GenericEvent $event)
{
+ // Avoid infinite loop, a listener instance can be called only one time
+ if ($this->called) {
+ return false;
+ }
+
+ $data = $event->getAll();
+
if ($this->isExecutable($data)) {
+ $this->called = true;
return $this->doAction($data);
}
diff --git a/app/Action/CommentCreation.php b/app/Action/CommentCreation.php
new file mode 100644
index 00000000..54d7be7d
--- /dev/null
+++ b/app/Action/CommentCreation.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Action;
+
+use Integration\GithubWebhook;
+
+/**
+ * Create automatically a comment from a webhook
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class CommentCreation extends Base
+{
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ GithubWebhook::EVENT_ISSUE_COMMENT,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getActionRequiredParameters()
+ {
+ return array();
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return array
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'reference',
+ 'comment',
+ 'user_id',
+ 'task_id',
+ );
+ }
+
+ /**
+ * Execute the action (create a new comment)
+ *
+ * @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 (bool) $this->comment->create(array(
+ 'reference' => $data['reference'],
+ 'comment' => $data['comment'],
+ 'task_id' => $data['task_id'],
+ 'user_id' => $data['user_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 true;
+ }
+}
diff --git a/app/Action/TaskAssignCategoryColor.php b/app/Action/TaskAssignCategoryColor.php
index be15f659..ba319a1f 100644
--- a/app/Action/TaskAssignCategoryColor.php
+++ b/app/Action/TaskAssignCategoryColor.php
@@ -67,7 +67,7 @@ class TaskAssignCategoryColor extends Base
'category_id' => $this->getParam('category_id'),
);
- return $this->task->update($values, false);
+ return $this->taskModification->update($values);
}
/**
diff --git a/app/Action/TaskAssignCategoryLabel.php b/app/Action/TaskAssignCategoryLabel.php
index 5e1b025e..1383d491 100644
--- a/app/Action/TaskAssignCategoryLabel.php
+++ b/app/Action/TaskAssignCategoryLabel.php
@@ -2,7 +2,7 @@
namespace Action;
-use Model\GithubWebhook;
+use Integration\GithubWebhook;
/**
* Set a category automatically according to a label
@@ -67,7 +67,7 @@ class TaskAssignCategoryLabel extends Base
'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'),
);
- return $this->task->update($values, false);
+ return $this->taskModification->update($values);
}
/**
diff --git a/app/Action/TaskAssignColorCategory.php b/app/Action/TaskAssignColorCategory.php
index f5a9ac5a..a362c68f 100644
--- a/app/Action/TaskAssignColorCategory.php
+++ b/app/Action/TaskAssignColorCategory.php
@@ -67,7 +67,7 @@ class TaskAssignColorCategory extends Base
'color_id' => $this->getParam('color_id'),
);
- return $this->task->update($values, false);
+ return $this->taskModification->update($values);
}
/**
diff --git a/app/Action/TaskAssignColorColumn.php b/app/Action/TaskAssignColorColumn.php
new file mode 100644
index 00000000..deb3e2b0
--- /dev/null
+++ b/app/Action/TaskAssignColorColumn.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Action;
+
+use Model\Task;
+
+/**
+ * Assign a color to a task
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class TaskAssignColorColumn extends Base
+{
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ Task::EVENT_CREATE,
+ 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'),
+ 'color_id' => t('Color'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'column_id',
+ );
+ }
+
+ /**
+ * Execute the action (set the task color)
+ *
+ * @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'],
+ 'color_id' => $this->getParam('color_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)
+ {
+ return $data['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/app/Action/TaskAssignColorUser.php b/app/Action/TaskAssignColorUser.php
index 00680186..6161514d 100644
--- a/app/Action/TaskAssignColorUser.php
+++ b/app/Action/TaskAssignColorUser.php
@@ -68,7 +68,7 @@ class TaskAssignColorUser extends Base
'color_id' => $this->getParam('color_id'),
);
- return $this->task->update($values, false);
+ return $this->taskModification->update($values);
}
/**
diff --git a/app/Action/TaskAssignCurrentUser.php b/app/Action/TaskAssignCurrentUser.php
index 7a9cf70b..ff3aaee1 100644
--- a/app/Action/TaskAssignCurrentUser.php
+++ b/app/Action/TaskAssignCurrentUser.php
@@ -62,12 +62,16 @@ class TaskAssignCurrentUser extends Base
*/
public function doAction(array $data)
{
+ if (! $this->userSession->isLogged()) {
+ return false;
+ }
+
$values = array(
'id' => $data['task_id'],
- 'owner_id' => $this->acl->getUserId(),
+ 'owner_id' => $this->userSession->getId(),
);
- return $this->task->update($values, false);
+ return $this->taskModification->update($values);
}
/**
diff --git a/app/Action/TaskAssignSpecificUser.php b/app/Action/TaskAssignSpecificUser.php
index f70459be..4c96f7f0 100644
--- a/app/Action/TaskAssignSpecificUser.php
+++ b/app/Action/TaskAssignSpecificUser.php
@@ -68,7 +68,7 @@ class TaskAssignSpecificUser extends Base
'owner_id' => $this->getParam('user_id'),
);
- return $this->task->update($values, false);
+ return $this->taskModification->update($values);
}
/**
diff --git a/app/Action/TaskAssignUser.php b/app/Action/TaskAssignUser.php
index 29ea91e6..cf2a9a4b 100644
--- a/app/Action/TaskAssignUser.php
+++ b/app/Action/TaskAssignUser.php
@@ -2,7 +2,7 @@
namespace Action;
-use Model\GithubWebhook;
+use Integration\GithubWebhook;
/**
* Assign a task to someone
@@ -64,7 +64,7 @@ class TaskAssignUser extends Base
'owner_id' => $data['owner_id'],
);
- return $this->task->update($values, false);
+ return $this->taskModification->update($values);
}
/**
diff --git a/app/Action/TaskClose.php b/app/Action/TaskClose.php
index f71d4b0e..b7cd4dbf 100644
--- a/app/Action/TaskClose.php
+++ b/app/Action/TaskClose.php
@@ -2,7 +2,9 @@
namespace Action;
-use Model\GithubWebhook;
+use Integration\GitlabWebhook;
+use Integration\GithubWebhook;
+use Integration\BitbucketWebhook;
use Model\Task;
/**
@@ -25,6 +27,9 @@ class TaskClose extends Base
Task::EVENT_MOVE_COLUMN,
GithubWebhook::EVENT_COMMIT,
GithubWebhook::EVENT_ISSUE_CLOSED,
+ GitlabWebhook::EVENT_COMMIT,
+ GitlabWebhook::EVENT_ISSUE_CLOSED,
+ BitbucketWebhook::EVENT_COMMIT,
);
}
@@ -39,6 +44,9 @@ class TaskClose extends Base
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:
return array();
default:
return array('column_id' => t('Column'));
@@ -56,6 +64,9 @@ class TaskClose extends Base
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:
return array('task_id');
default:
return array('task_id', 'column_id');
@@ -71,7 +82,7 @@ class TaskClose extends Base
*/
public function doAction(array $data)
{
- return $this->task->close($data['task_id']);
+ return $this->taskStatus->close($data['task_id']);
}
/**
@@ -86,6 +97,9 @@ class TaskClose extends Base
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:
return true;
default:
return $data['column_id'] == $this->getParam('column_id');
diff --git a/app/Action/TaskCreation.php b/app/Action/TaskCreation.php
index 41d0200c..1c093eee 100644
--- a/app/Action/TaskCreation.php
+++ b/app/Action/TaskCreation.php
@@ -2,7 +2,8 @@
namespace Action;
-use Model\GithubWebhook;
+use Integration\GithubWebhook;
+use Integration\GitlabWebhook;
/**
* Create automatically a task from a webhook
@@ -22,6 +23,7 @@ class TaskCreation extends Base
{
return array(
GithubWebhook::EVENT_ISSUE_OPENED,
+ GitlabWebhook::EVENT_ISSUE_OPENED,
);
}
@@ -59,11 +61,11 @@ class TaskCreation extends Base
*/
public function doAction(array $data)
{
- return $this->task->create(array(
+ return (bool) $this->taskCreation->create(array(
'project_id' => $data['project_id'],
'title' => $data['title'],
'reference' => $data['reference'],
- 'description' => $data['description'],
+ 'description' => isset($data['description']) ? $data['description'] : '',
));
}
diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php
index 4ab88534..55ebc76e 100644
--- a/app/Action/TaskDuplicateAnotherProject.php
+++ b/app/Action/TaskDuplicateAnotherProject.php
@@ -64,9 +64,7 @@ class TaskDuplicateAnotherProject extends Base
*/
public function doAction(array $data)
{
- $task = $this->taskFinder->getById($data['task_id']);
- $this->task->duplicateToAnotherProject($this->getParam('project_id'), $task);
- return true;
+ return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'));
}
/**
diff --git a/app/Action/TaskLogMoveAnotherColumn.php b/app/Action/TaskLogMoveAnotherColumn.php
new file mode 100644
index 00000000..621e8e6c
--- /dev/null
+++ b/app/Action/TaskLogMoveAnotherColumn.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Action;
+
+use Model\GithubWebhook;
+use Model\Task;
+
+/**
+ * Add a log of the triggering event to the task description.
+ *
+ * @package action
+ * @author Oren Ben-Kiki
+ */
+class TaskLogMoveAnotherColumn extends Base
+{
+ /**
+ * 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 (append to the task description).
+ *
+ * @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;
+ }
+
+ $column = $this->board->getColumn($data['column_id']);
+
+ return (bool) $this->comment->create(array(
+ 'comment' => t('Moved to column %s', $column['title']),
+ 'task_id' => $data['task_id'],
+ 'user_id' => $this->userSession->getId(),
+ ));
+ }
+
+ /**
+ * 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/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php
index d852f56d..ee212998 100644
--- a/app/Action/TaskMoveAnotherProject.php
+++ b/app/Action/TaskMoveAnotherProject.php
@@ -64,9 +64,7 @@ class TaskMoveAnotherProject extends Base
*/
public function doAction(array $data)
{
- $task = $this->taskFinder->getById($data['task_id']);
- $this->task->moveToAnotherProject($this->getParam('project_id'), $task);
- return true;
+ return $this->taskDuplication->moveToProject($data['task_id'], $this->getParam('project_id'));
}
/**
diff --git a/app/Action/TaskMoveColumnAssigned.php b/app/Action/TaskMoveColumnAssigned.php
new file mode 100644
index 00000000..decf4b01
--- /dev/null
+++ b/app/Action/TaskMoveColumnAssigned.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Action;
+
+use Model\Task;
+
+/**
+ * Move a task to another column when an assignee is set
+ *
+ * @package action
+ * @author Francois Ferrand
+ */
+class TaskMoveColumnAssigned extends Base
+{
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ Task::EVENT_ASSIGNEE_CHANGE,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'src_column_id' => t('Source column'),
+ 'dest_column_id' => t('Destination column')
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'column_id',
+ 'project_id',
+ 'owner_id'
+ );
+ }
+
+ /**
+ * 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)
+ {
+ $original_task = $this->taskFinder->getById($data['task_id']);
+
+ return $this->taskPosition->movePosition(
+ $data['project_id'],
+ $data['task_id'],
+ $this->getParam('dest_column_id'),
+ $original_task['position'],
+ $original_task['swimlane_id'],
+ false
+ );
+ }
+
+ /**
+ * 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('src_column_id') && $data['owner_id'];
+ }
+}
diff --git a/app/Action/TaskMoveColumnUnAssigned.php b/app/Action/TaskMoveColumnUnAssigned.php
new file mode 100644
index 00000000..b773252d
--- /dev/null
+++ b/app/Action/TaskMoveColumnUnAssigned.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Action;
+
+use Model\Task;
+
+/**
+ * Move a task to another column when an assignee is cleared
+ *
+ * @package action
+ * @author Francois Ferrand
+ */
+class TaskMoveColumnUnAssigned extends Base
+{
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ Task::EVENT_ASSIGNEE_CHANGE
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'src_column_id' => t('Source column'),
+ 'dest_column_id' => t('Destination column')
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'column_id',
+ 'project_id',
+ 'owner_id'
+ );
+ }
+
+ /**
+ * 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)
+ {
+ $original_task = $this->taskFinder->getById($data['task_id']);
+
+ return $this->taskPosition->movePosition(
+ $data['project_id'],
+ $data['task_id'],
+ $this->getParam('dest_column_id'),
+ $original_task['position'],
+ $original_task['swimlane_id'],
+ false
+ );
+ }
+
+ /**
+ * 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('src_column_id') && ! $data['owner_id'];
+ }
+}
diff --git a/app/Action/TaskOpen.php b/app/Action/TaskOpen.php
index 6847856c..73f1fad3 100644
--- a/app/Action/TaskOpen.php
+++ b/app/Action/TaskOpen.php
@@ -2,7 +2,7 @@
namespace Action;
-use Model\GithubWebhook;
+use Integration\GithubWebhook;
/**
* Open automatically a task
@@ -56,7 +56,7 @@ class TaskOpen extends Base
*/
public function doAction(array $data)
{
- return $this->task->open($data['task_id']);
+ return $this->taskStatus->open($data['task_id']);
}
/**
diff --git a/app/Action/TaskUpdateStartDate.php b/app/Action/TaskUpdateStartDate.php
new file mode 100644
index 00000000..4cd50c9a
--- /dev/null
+++ b/app/Action/TaskUpdateStartDate.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Action;
+
+use Model\Task;
+
+/**
+ * Set the start date of task
+ *
+ * @package action
+ * @author Frederic Guillot
+ */
+class TaskUpdateStartDate extends Base
+{
+ /**
+ * 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 (set the task color)
+ *
+ * @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'],
+ 'date_started' => time(),
+ );
+
+ 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/Api/Action.php b/app/Api/Action.php
new file mode 100644
index 00000000..6187b776
--- /dev/null
+++ b/app/Api/Action.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Api;
+
+/**
+ * Action API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Action extends Base
+{
+ public function getAvailableActions()
+ {
+ return $this->action->getAvailableActions();
+ }
+
+ public function getAvailableActionEvents()
+ {
+ return $this->action->getAvailableEvents();
+ }
+
+ public function getCompatibleActionEvents($action_name)
+ {
+ return $this->action->getCompatibleEvents($action_name);
+ }
+
+ public function removeAction($action_id)
+ {
+ return $this->action->remove($action_id);
+ }
+
+ 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;
+ }
+
+ public function createAction($project_id, $event_name, $action_name, $params)
+ {
+ $values = array(
+ 'project_id' => $project_id,
+ 'event_name' => $event_name,
+ 'action_name' => $action_name,
+ 'params' => $params,
+ );
+
+ list($valid,) = $this->action->validateCreation($values);
+
+ if (! $valid) {
+ return false;
+ }
+
+ // Check if the action exists
+ $actions = $this->action->getAvailableActions();
+
+ if (! isset($actions[$action_name])) {
+ return false;
+ }
+
+ // Check the event
+ $action = $this->action->load($action_name, $project_id, $event_name);
+
+ if (! in_array($event_name, $action->getCompatibleEvents())) {
+ return false;
+ }
+
+ $required_params = $action->getActionRequiredParameters();
+
+ // Check missing parameters
+ foreach($required_params as $param => $value) {
+ if (! isset($params[$param])) {
+ return false;
+ }
+ }
+
+ // Check extra parameters
+ foreach($params as $param => $value) {
+ if (! isset($required_params[$param])) {
+ return false;
+ }
+ }
+
+ return $this->action->create($values);
+ }
+}
diff --git a/app/Api/App.php b/app/Api/App.php
new file mode 100644
index 00000000..2fc32e91
--- /dev/null
+++ b/app/Api/App.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Api;
+
+/**
+ * App API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class App extends Base
+{
+ public function getTimezone()
+ {
+ return $this->config->get('application_timezone');
+ }
+
+ public function getVersion()
+ {
+ return APP_VERSION;
+ }
+}
diff --git a/app/Api/Base.php b/app/Api/Base.php
new file mode 100644
index 00000000..445b35be
--- /dev/null
+++ b/app/Api/Base.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Api;
+
+use JsonRPC\AuthenticationFailure;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Base class
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+abstract class Base extends \Core\Base
+{
+ /**
+ * Check api credentials
+ *
+ * @access public
+ * @param string $username
+ * @param string $password
+ * @param string $class
+ * @param string $method
+ */
+ public function authentication($username, $password, $class, $method)
+ {
+ $this->container['dispatcher']->dispatch('api.bootstrap', new Event);
+
+ if (! ($username === 'jsonrpc' && $password === $this->config->get('api_token'))) {
+ throw new AuthenticationFailure('Wrong credentials');
+ }
+ }
+}
diff --git a/app/Api/Board.php b/app/Api/Board.php
new file mode 100644
index 00000000..163131b6
--- /dev/null
+++ b/app/Api/Board.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Api;
+
+/**
+ * Board API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Board extends Base
+{
+ public function getBoard($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
new file mode 100644
index 00000000..f457ddf1
--- /dev/null
+++ b/app/Api/Category.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Api;
+
+/**
+ * Category API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Category extends Base
+{
+ public function getCategory($category_id)
+ {
+ return $this->category->getById($category_id);
+ }
+
+ public function getAllCategories($project_id)
+ {
+ return $this->category->getAll($project_id);
+ }
+
+ public function removeCategory($category_id)
+ {
+ return $this->category->remove($category_id);
+ }
+
+ public function createCategory($project_id, $name)
+ {
+ $values = array(
+ 'project_id' => $project_id,
+ 'name' => $name,
+ );
+
+ list($valid,) = $this->category->validateCreation($values);
+ return $valid ? $this->category->create($values) : false;
+ }
+
+ public function updateCategory($id, $name)
+ {
+ $values = array(
+ 'id' => $id,
+ 'name' => $name,
+ );
+
+ list($valid,) = $this->category->validateModification($values);
+ return $valid && $this->category->update($values);
+ }
+}
diff --git a/app/Api/Comment.php b/app/Api/Comment.php
new file mode 100644
index 00000000..19b84383
--- /dev/null
+++ b/app/Api/Comment.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Api;
+
+/**
+ * Comment API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Comment extends Base
+{
+ public function getComment($comment_id)
+ {
+ return $this->comment->getById($comment_id);
+ }
+
+ public function getAllComments($task_id)
+ {
+ return $this->comment->getAll($task_id);
+ }
+
+ public function removeComment($comment_id)
+ {
+ return $this->comment->remove($comment_id);
+ }
+
+ public function createComment($task_id, $user_id, $content)
+ {
+ $values = array(
+ 'task_id' => $task_id,
+ 'user_id' => $user_id,
+ 'comment' => $content,
+ );
+
+ list($valid,) = $this->comment->validateCreation($values);
+
+ return $valid ? $this->comment->create($values) : false;
+ }
+
+ public function updateComment($id, $content)
+ {
+ $values = array(
+ 'id' => $id,
+ 'comment' => $content,
+ );
+
+ list($valid,) = $this->comment->validateModification($values);
+ return $valid && $this->comment->update($values);
+ }
+}
diff --git a/app/Api/File.php b/app/Api/File.php
new file mode 100644
index 00000000..65dff729
--- /dev/null
+++ b/app/Api/File.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Api;
+
+/**
+ * File API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class File extends Base
+{
+ public function getFile($file_id)
+ {
+ return $this->file->getById($file_id);
+ }
+
+ public function getAllFiles($task_id)
+ {
+ return $this->file->getAll($task_id);
+ }
+
+ public function downloadFile($file_id)
+ {
+ $file = $this->file->getById($file_id);
+
+ if (! empty($file)) {
+
+ $filename = FILES_DIR.$file['path'];
+
+ if (file_exists($filename)) {
+ return base64_encode(file_get_contents($filename));
+ }
+ }
+
+ return '';
+ }
+
+ public function createFile($project_id, $task_id, $filename, $is_image, $blob)
+ {
+ return $this->file->uploadContent($project_id, $task_id, $filename, $is_image, $blob);
+ }
+
+ public function removeFile($file_id)
+ {
+ return $this->file->remove($file_id);
+ }
+}
diff --git a/app/Api/Link.php b/app/Api/Link.php
new file mode 100644
index 00000000..b9084784
--- /dev/null
+++ b/app/Api/Link.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Api;
+
+/**
+ * Link API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Link extends Base
+{
+ /**
+ * Get a link by id
+ *
+ * @access public
+ * @param integer $link_id Link id
+ * @return array
+ */
+ public function getLinkById($link_id)
+ {
+ return $this->link->getById($link_id);
+ }
+
+ /**
+ * Get a link by name
+ *
+ * @access public
+ * @param string $label
+ * @return array
+ */
+ public function getLinkByLabel($label)
+ {
+ return $this->link->getByLabel($label);
+ }
+
+ /**
+ * Get the opposite link id
+ *
+ * @access public
+ * @param integer $link_id Link id
+ * @return integer
+ */
+ public function getOppositeLinkId($link_id)
+ {
+ return $this->link->getOppositeLinkId($link_id);
+ }
+
+ /**
+ * Get all links
+ *
+ * @access public
+ * @return array
+ */
+ public function getAllLinks()
+ {
+ return $this->link->getAll();
+ }
+
+ /**
+ * Create a new link label
+ *
+ * @access public
+ * @param string $label
+ * @param string $opposite_label
+ * @return boolean|integer
+ */
+ public function createLink($label, $opposite_label = '')
+ {
+ $values = array(
+ 'label' => $label,
+ 'opposite_label' => $opposite_label,
+ );
+
+ list($valid,) = $this->link->validateCreation($values);
+ return $valid ? $this->link->create($label, $opposite_label) : false;
+ }
+
+ /**
+ * Update a link
+ *
+ * @access public
+ * @param integer $link_id
+ * @param integer $opposite_link_id
+ * @param string $label
+ * @return boolean
+ */
+ public function updateLink($link_id, $opposite_link_id, $label)
+ {
+ $values = array(
+ 'id' => $link_id,
+ 'opposite_id' => $opposite_link_id,
+ 'label' => $label,
+ );
+
+ list($valid,) = $this->link->validateModification($values);
+ return $valid && $this->link->update($values);
+ }
+
+ /**
+ * Remove a link a the relation to its opposite
+ *
+ * @access public
+ * @param integer $link_id
+ * @return boolean
+ */
+ public function removeLink($link_id)
+ {
+ return $this->link->remove($link_id);
+ }
+}
diff --git a/app/Api/Project.php b/app/Api/Project.php
new file mode 100644
index 00000000..2451cd9c
--- /dev/null
+++ b/app/Api/Project.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Api;
+
+/**
+ * Project API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Project extends Base
+{
+ public function getProjectById($project_id)
+ {
+ return $this->project->getById($project_id);
+ }
+
+ public function getProjectByName($name)
+ {
+ return $this->project->getByName($name);
+ }
+
+ public function getAllProjects()
+ {
+ return $this->project->getAll();
+ }
+
+ public function removeProject($project_id)
+ {
+ return $this->project->remove($project_id);
+ }
+
+ public function enableProject($project_id)
+ {
+ return $this->project->enable($project_id);
+ }
+
+ public function disableProject($project_id)
+ {
+ return $this->project->disable($project_id);
+ }
+
+ public function enableProjectPublicAccess($project_id)
+ {
+ return $this->project->enablePublicAccess($project_id);
+ }
+
+ public function disableProjectPublicAccess($project_id)
+ {
+ return $this->project->disablePublicAccess($project_id);
+ }
+
+ public function getProjectActivities(array $project_ids)
+ {
+ return $this->projectActivity->getProjects($project_ids);
+ }
+
+ public function getProjectActivity($project_id)
+ {
+ return $this->projectActivity->getProject($project_id);
+ }
+
+ public function createProject($name, $description = null)
+ {
+ $values = array(
+ 'name' => $name,
+ 'description' => $description
+ );
+
+ list($valid,) = $this->project->validateCreation($values);
+ return $valid ? $this->project->create($values) : false;
+ }
+
+ public function updateProject($id, $name, $description = null)
+ {
+ $values = array(
+ 'id' => $id,
+ 'name' => $name,
+ 'description' => $description
+ );
+
+ list($valid,) = $this->project->validateModification($values);
+ return $valid && $this->project->update($values);
+ }
+}
diff --git a/app/Api/ProjectPermission.php b/app/Api/ProjectPermission.php
new file mode 100644
index 00000000..a31faf3d
--- /dev/null
+++ b/app/Api/ProjectPermission.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Api;
+
+/**
+ * ProjectPermission API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class ProjectPermission extends Base
+{
+ public function getMembers($project_id)
+ {
+ return $this->projectPermission->getMembers($project_id);
+ }
+
+ public function revokeUser($project_id, $user_id)
+ {
+ return $this->projectPermission->revokeMember($project_id, $user_id);
+ }
+
+ public function allowUser($project_id, $user_id)
+ {
+ return $this->projectPermission->addMember($project_id, $user_id);
+ }
+}
diff --git a/app/Api/Subtask.php b/app/Api/Subtask.php
new file mode 100644
index 00000000..2e6c30f2
--- /dev/null
+++ b/app/Api/Subtask.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Api;
+
+/**
+ * Subtask API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Subtask extends Base
+{
+ public function getSubtask($subtask_id)
+ {
+ return $this->subtask->getById($subtask_id);
+ }
+
+ public function getAllSubtasks($task_id)
+ {
+ return $this->subtask->getAll($task_id);
+ }
+
+ public function removeSubtask($subtask_id)
+ {
+ return $this->subtask->remove($subtask_id);
+ }
+
+ public function createSubtask($task_id, $title, $user_id = 0, $time_estimated = 0, $time_spent = 0, $status = 0)
+ {
+ $values = array(
+ 'title' => $title,
+ 'task_id' => $task_id,
+ 'user_id' => $user_id,
+ 'time_estimated' => $time_estimated,
+ 'time_spent' => $time_spent,
+ 'status' => $status,
+ );
+
+ list($valid,) = $this->subtask->validateCreation($values);
+ return $valid ? $this->subtask->create($values) : false;
+ }
+
+ public function updateSubtask($id, $task_id, $title = null, $user_id = null, $time_estimated = null, $time_spent = null, $status = null)
+ {
+ $values = array(
+ 'id' => $id,
+ 'task_id' => $task_id,
+ 'title' => $title,
+ 'user_id' => $user_id,
+ 'time_estimated' => $time_estimated,
+ 'time_spent' => $time_spent,
+ 'status' => $status,
+ );
+
+ foreach ($values as $key => $value) {
+ if (is_null($value)) {
+ unset($values[$key]);
+ }
+ }
+
+ list($valid,) = $this->subtask->validateApiModification($values);
+ return $valid && $this->subtask->update($values);
+ }
+}
diff --git a/app/Api/Swimlane.php b/app/Api/Swimlane.php
new file mode 100644
index 00000000..322b0805
--- /dev/null
+++ b/app/Api/Swimlane.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Api;
+
+/**
+ * Swimlane API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Swimlane extends Base
+{
+ public function getActiveSwimlanes($project_id)
+ {
+ return $this->swimlane->getSwimlanes($project_id);
+ }
+
+ public function getAllSwimlanes($project_id)
+ {
+ return $this->swimlane->getAll($project_id);
+ }
+
+ public function getSwimlaneById($swimlane_id)
+ {
+ return $this->swimlane->getById($swimlane_id);
+ }
+
+ public function getSwimlaneByName($project_id, $name)
+ {
+ return $this->swimlane->getByName($project_id, $name);
+ }
+
+ public function getSwimlane($swimlane_id)
+ {
+ return $this->swimlane->getById($swimlane_id);
+ }
+
+ public function getDefaultSwimlane($project_id)
+ {
+ return $this->swimlane->getDefault($project_id);
+ }
+
+ public function addSwimlane($project_id, $name)
+ {
+ return $this->swimlane->create($project_id, $name);
+ }
+
+ public function updateSwimlane($swimlane_id, $name)
+ {
+ return $this->swimlane->rename($swimlane_id, $name);
+ }
+
+ public function removeSwimlane($project_id, $swimlane_id)
+ {
+ return $this->swimlane->remove($project_id, $swimlane_id);
+ }
+
+ public function disableSwimlane($project_id, $swimlane_id)
+ {
+ return $this->swimlane->disable($project_id, $swimlane_id);
+ }
+
+ public function enableSwimlane($project_id, $swimlane_id)
+ {
+ 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)
+ {
+ return $this->swimlane->moveDown($project_id, $swimlane_id);
+ }
+}
diff --git a/app/Api/Task.php b/app/Api/Task.php
new file mode 100644
index 00000000..c98b24a6
--- /dev/null
+++ b/app/Api/Task.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Api;
+
+use Model\Task as TaskModel;
+
+/**
+ * Task API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class Task extends Base
+{
+ public function getTask($task_id)
+ {
+ return $this->taskFinder->getById($task_id);
+ }
+
+ public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN)
+ {
+ return $this->taskFinder->getAll($project_id, $status_id);
+ }
+
+ public function getOverdueTasks()
+ {
+ return $this->taskFinder->getOverdueTasks();
+ }
+
+ public function openTask($task_id)
+ {
+ return $this->taskStatus->open($task_id);
+ }
+
+ public function closeTask($task_id)
+ {
+ return $this->taskStatus->close($task_id);
+ }
+
+ public function removeTask($task_id)
+ {
+ return $this->task->remove($task_id);
+ }
+
+ public function moveTaskPosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0)
+ {
+ return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_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)
+ {
+ $values = array(
+ 'title' => $title,
+ 'project_id' => $project_id,
+ 'color_id' => $color_id,
+ 'column_id' => $column_id,
+ 'owner_id' => $owner_id,
+ 'creator_id' => $creator_id,
+ 'date_due' => $date_due,
+ 'description' => $description,
+ 'category_id' => $category_id,
+ 'score' => $score,
+ 'swimlane_id' => $swimlane_id,
+ 'recurrence_status' => $recurrence_status,
+ 'recurrence_trigger' => $recurrence_trigger,
+ 'recurrence_factor' => $recurrence_factor,
+ 'recurrence_timeframe' => $recurrence_timeframe,
+ 'recurrence_basedate' => $recurrence_basedate,
+ );
+
+ list($valid,) = $this->taskValidator->validateCreation($values);
+
+ return $valid ? $this->taskCreation->create($values) : false;
+ }
+
+ public function updateTask($id, $title = null, $project_id = null, $color_id = null, $column_id = null, $owner_id = null,
+ $creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null,
+ $swimlane_id = null, $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null,
+ $recurrence_timeframe = null, $recurrence_basedate = null)
+ {
+ $values = array(
+ 'id' => $id,
+ 'title' => $title,
+ 'project_id' => $project_id,
+ 'color_id' => $color_id,
+ 'column_id' => $column_id,
+ 'owner_id' => $owner_id,
+ 'creator_id' => $creator_id,
+ 'date_due' => $date_due,
+ 'description' => $description,
+ 'category_id' => $category_id,
+ 'score' => $score,
+ 'swimlane_id' => $swimlane_id,
+ 'recurrence_status' => $recurrence_status,
+ 'recurrence_trigger' => $recurrence_trigger,
+ 'recurrence_factor' => $recurrence_factor,
+ 'recurrence_timeframe' => $recurrence_timeframe,
+ 'recurrence_basedate' => $recurrence_basedate,
+ );
+
+ foreach ($values as $key => $value) {
+ if (is_null($value)) {
+ unset($values[$key]);
+ }
+ }
+
+ list($valid) = $this->taskValidator->validateApiModification($values);
+ return $valid && $this->taskModification->update($values);
+ }
+}
diff --git a/app/Api/TaskLink.php b/app/Api/TaskLink.php
new file mode 100644
index 00000000..c3e1a83c
--- /dev/null
+++ b/app/Api/TaskLink.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Api;
+
+/**
+ * TaskLink API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class TaskLink extends Base
+{
+ /**
+ * Get a task link
+ *
+ * @access public
+ * @param integer $task_link_id Task link id
+ * @return array
+ */
+ public function getTaskLinkById($task_link_id)
+ {
+ return $this->taskLink->getById($task_link_id);
+ }
+
+ /**
+ * Get all links attached to a task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getAllTaskLinks($task_id)
+ {
+ return $this->taskLink->getAll($task_id);
+ }
+
+ /**
+ * Create a new link
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param integer $opposite_task_id Opposite task id
+ * @param integer $link_id Link id
+ * @return integer Task link id
+ */
+ public function createTaskLink($task_id, $opposite_task_id, $link_id)
+ {
+ return $this->taskLink->create($task_id, $opposite_task_id, $link_id);
+ }
+
+ /**
+ * Update a task link
+ *
+ * @access public
+ * @param integer $task_link_id Task link id
+ * @param integer $task_id Task id
+ * @param integer $opposite_task_id Opposite task id
+ * @param integer $link_id Link id
+ * @return boolean
+ */
+ public function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id)
+ {
+ return $this->taskLink->update($task_link_id, $task_id, $opposite_task_id, $link_id);
+ }
+
+ /**
+ * Remove a link between two tasks
+ *
+ * @access public
+ * @param integer $task_link_id
+ * @return boolean
+ */
+ public function removeTaskLink($task_link_id)
+ {
+ return $this->taskLink->remove($task_link_id);
+ }
+}
diff --git a/app/Api/User.php b/app/Api/User.php
new file mode 100644
index 00000000..166ef2c1
--- /dev/null
+++ b/app/Api/User.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Api;
+
+use Auth\Ldap;
+
+/**
+ * User API controller
+ *
+ * @package api
+ * @author Frederic Guillot
+ */
+class User extends Base
+{
+ public function getUser($user_id)
+ {
+ return $this->user->getById($user_id);
+ }
+
+ public function getAllUsers()
+ {
+ return $this->user->getAll();
+ }
+
+ public function removeUser($user_id)
+ {
+ return $this->user->remove($user_id);
+ }
+
+ public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $default_project_id = 0)
+ {
+ $values = array(
+ 'username' => $username,
+ 'password' => $password,
+ 'confirmation' => $password,
+ 'name' => $name,
+ 'email' => $email,
+ 'is_admin' => $is_admin,
+ 'default_project_id' => $default_project_id,
+ );
+
+ list($valid,) = $this->user->validateCreation($values);
+
+ return $valid ? $this->user->create($values) : false;
+ }
+
+ public function createLdapUser($username = '', $email = '', $is_admin = 0, $default_project_id = 0)
+ {
+ $ldap = new Ldap($this->container);
+ $user = $ldap->lookup($username, $email);
+
+ if (! $user) {
+ return false;
+ }
+
+ $values = array(
+ 'username' => $user['username'],
+ 'name' => $user['name'],
+ 'email' => $user['email'],
+ 'is_ldap_user' => 1,
+ 'is_admin' => $is_admin,
+ 'default_project_id' => $default_project_id,
+ );
+
+ return $this->user->create($values);
+ }
+
+ public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $default_project_id = null)
+ {
+ $values = array(
+ 'id' => $id,
+ 'username' => $username,
+ 'name' => $name,
+ 'email' => $email,
+ 'is_admin' => $is_admin,
+ 'default_project_id' => $default_project_id,
+ );
+
+ foreach ($values as $key => $value) {
+ if (is_null($value)) {
+ unset($values[$key]);
+ }
+ }
+
+ list($valid,) = $this->user->validateApiModification($values);
+ return $valid && $this->user->update($values);
+ }
+}
diff --git a/app/Auth/Base.php b/app/Auth/Base.php
index e174ff8f..ebf6681b 100644
--- a/app/Auth/Base.php
+++ b/app/Auth/Base.php
@@ -2,20 +2,15 @@
namespace Auth;
-use Core\Tool;
-use Core\Registry;
+use Pimple\Container;
/**
* Base auth class
*
* @package auth
* @author Frederic Guillot
- *
- * @property \Model\Acl $acl
- * @property \Model\LastLogin $lastLogin
- * @property \Model\User $user
*/
-abstract class Base
+abstract class Base extends \Core\Base
{
/**
* Database instance
@@ -26,34 +21,14 @@ abstract class Base
protected $db;
/**
- * Registry instance
- *
- * @access protected
- * @var \Core\Registry
- */
- protected $registry;
-
- /**
* Constructor
*
* @access public
- * @param \Core\Registry $registry Registry instance
- */
- public function __construct(Registry $registry)
- {
- $this->registry = $registry;
- $this->db = $this->registry->shared('db');
- }
-
- /**
- * Load automatically models
- *
- * @access public
- * @param string $name Model name
- * @return mixed
+ * @param \Pimple\Container $container
*/
- public function __get($name)
+ public function __construct(Container $container)
{
- return Tool::loadModel($this->registry, $name);
+ $this->container = $container;
+ $this->db = $this->container['db'];
}
}
diff --git a/app/Auth/Database.php b/app/Auth/Database.php
index 47dc8e6e..e69f18a9 100644
--- a/app/Auth/Database.php
+++ b/app/Auth/Database.php
@@ -3,7 +3,7 @@
namespace Auth;
use Model\User;
-use Core\Request;
+use Event\AuthEvent;
/**
* Database authentication
@@ -30,21 +30,16 @@ class Database extends Base
*/
public function authenticate($username, $password)
{
- $user = $this->db->table(User::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne();
-
- if ($user && password_verify($password, $user['password'])) {
-
- // Update user session
- $this->user->updateSession($user);
-
- // Update login history
- $this->lastLogin->create(
- self::AUTH_NAME,
- $user['id'],
- Request::getIpAddress(),
- Request::getUserAgent()
- );
-
+ $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;
}
diff --git a/app/Auth/GitHub.php b/app/Auth/GitHub.php
index 096d4101..816cc9c1 100644
--- a/app/Auth/GitHub.php
+++ b/app/Auth/GitHub.php
@@ -2,9 +2,7 @@
namespace Auth;
-require __DIR__.'/../../vendor/OAuth/bootstrap.php';
-
-use Core\Request;
+use Event\AuthEvent;
use OAuth\Common\Storage\Session;
use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Http\Uri\UriFactory;
@@ -36,19 +34,9 @@ class GitHub extends Base
{
$user = $this->user->getByGitHubId($github_id);
- if ($user) {
-
- // Create the user session
- $this->user->updateSession($user);
-
- // Update login history
- $this->lastLogin->create(
- self::AUTH_NAME,
- $user['id'],
- Request::getIpAddress(),
- Request::getUserAgent()
- );
-
+ if (! empty($user)) {
+ $this->userSession->refresh($user);
+ $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
diff --git a/app/Auth/Google.php b/app/Auth/Google.php
index 2bed5b09..9a977037 100644
--- a/app/Auth/Google.php
+++ b/app/Auth/Google.php
@@ -2,9 +2,7 @@
namespace Auth;
-require __DIR__.'/../../vendor/OAuth/bootstrap.php';
-
-use Core\Request;
+use Event\AuthEvent;
use OAuth\Common\Storage\Session;
use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Http\Uri\UriFactory;
@@ -37,19 +35,9 @@ class Google extends Base
{
$user = $this->user->getByGoogleId($google_id);
- if ($user) {
-
- // Create the user session
- $this->user->updateSession($user);
-
- // Update login history
- $this->lastLogin->create(
- self::AUTH_NAME,
- $user['id'],
- Request::getIpAddress(),
- Request::getUserAgent()
- );
-
+ if (! empty($user)) {
+ $this->userSession->refresh($user);
+ $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
diff --git a/app/Auth/Ldap.php b/app/Auth/Ldap.php
index 4f20998f..3ee6ec9b 100644
--- a/app/Auth/Ldap.php
+++ b/app/Auth/Ldap.php
@@ -2,7 +2,7 @@
namespace Auth;
-use Core\Request;
+use Event\AuthEvent;
/**
* LDAP model
@@ -29,13 +29,14 @@ class Ldap extends Base
*/
public function authenticate($username, $password)
{
+ $username = LDAP_USERNAME_CASE_SENSITIVE ? $username : strtolower($username);
$result = $this->findUser($username, $password);
if (is_array($result)) {
$user = $this->user->getByUsername($username);
- if ($user) {
+ if (! empty($user)) {
// There is already a local user with that name
if ($user['is_ldap_user'] == 0) {
@@ -54,15 +55,8 @@ class Ldap extends Base
}
// We open the session
- $this->user->updateSession($user);
-
- // Update login history
- $this->lastLogin->create(
- self::AUTH_NAME,
- $user['id'],
- Request::getIpAddress(),
- Request::getUserAgent()
- );
+ $this->userSession->refresh($user);
+ $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
@@ -104,7 +98,7 @@ class Ldap extends Base
{
$ldap = $this->connect();
- if ($this->bind($ldap, $username, $password)) {
+ if (is_resource($ldap) && $this->bind($ldap, $username, $password)) {
return $this->search($ldap, $username, $password);
}
@@ -136,6 +130,12 @@ class Ldap extends Base
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)) {
+ die('Unable to use ldap_start_tls()');
+ }
return $ldap;
}
@@ -200,11 +200,90 @@ class Ldap extends Base
return array(
'username' => $username,
- 'name' => isset($info[0][LDAP_ACCOUNT_FULLNAME][0]) ? $info[0][LDAP_ACCOUNT_FULLNAME][0] : '',
- 'email' => isset($info[0][LDAP_ACCOUNT_EMAIL][0]) ? $info[0][LDAP_ACCOUNT_EMAIL][0] : '',
+ 'name' => $this->getFromInfo($info, LDAP_ACCOUNT_FULLNAME),
+ 'email' => $this->getFromInfo($info, LDAP_ACCOUNT_EMAIL),
);
}
return false;
}
+
+ /**
+ * Retrieve info on LDAP user
+ *
+ * @param string $username Username
+ * @param string $email Email address
+ */
+ public function lookup($username = null, $email = null)
+ {
+ $query = $this->getQuery($username, $email);
+ if ($query === false) {
+ return false;
+ }
+
+ // Connect and attempt anonymous bind
+ $ldap = $this->connect();
+ if (! is_resource($ldap) || ! $this->bind($ldap, null, null)) {
+ return false;
+ }
+
+ // Try to find user
+ $sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, $query, array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL, LDAP_ACCOUNT_ID));
+ if ($sr === false) {
+ return false;
+ }
+
+ $info = ldap_get_entries($ldap, $sr);
+
+ // User not found
+ if (count($info) == 0 || $info['count'] == 0) {
+ return false;
+ }
+
+ // User id not retrieved: LDAP_ACCOUNT_ID not properly configured
+ if (empty($username) && ! isset($info[0][LDAP_ACCOUNT_ID][0])) {
+ return false;
+ }
+
+ return array(
+ 'username' => $this->getFromInfo($info, LDAP_ACCOUNT_ID, $username),
+ 'name' => $this->getFromInfo($info, LDAP_ACCOUNT_FULLNAME),
+ 'email' => $this->getFromInfo($info, LDAP_ACCOUNT_EMAIL, $email),
+ );
+ }
+
+ /**
+ * Get the LDAP query to find a user
+ *
+ * @param string $username Username
+ * @param string $email Email address
+ */
+ private function getQuery($username, $email)
+ {
+ if ($username && $email) {
+ return '(&('.sprintf(LDAP_USER_PATTERN, $username).')('.LDAP_ACCOUNT_EMAIL.'='.$email.'))';
+ }
+ else if ($username) {
+ return sprintf(LDAP_USER_PATTERN, $username);
+ }
+ else if ($email) {
+ return '('.LDAP_ACCOUNT_EMAIL.'='.$email.')';
+ }
+ else {
+ return false;
+ }
+ }
+
+ /**
+ * Return a value from the LDAP info
+ *
+ * @param array $info LDAP info
+ * @param string $key Key
+ * @param string $default Default value if key not set in entry
+ * @return string
+ */
+ private function getFromInfo($info, $key, $default = '')
+ {
+ return isset($info[0][$key][0]) ? $info[0][$key][0] : $default;
+ }
}
diff --git a/app/Auth/RememberMe.php b/app/Auth/RememberMe.php
index 380abbed..e8b20f37 100644
--- a/app/Auth/RememberMe.php
+++ b/app/Auth/RememberMe.php
@@ -3,8 +3,8 @@
namespace Auth;
use Core\Request;
+use Event\AuthEvent;
use Core\Security;
-use Core\Tool;
/**
* RememberMe model
@@ -96,20 +96,19 @@ class RememberMe extends Base
// Update the sequence
$this->writeCookie(
$record['token'],
- $this->update($record['token'], $record['sequence']),
+ $this->update($record['token']),
$record['expiration']
);
// Create the session
- $this->user->updateSession($this->user->getById($record['user_id']));
- $this->acl->isRememberMe(true);
-
- // Update last login infos
- $this->lastLogin->create(
- self::AUTH_NAME,
- $this->acl->getUserId(),
- Request::getIpAddress(),
- Request::getUserAgent()
+ $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;
@@ -137,7 +136,7 @@ class RememberMe extends Base
// Update the sequence
$this->writeCookie(
$record['token'],
- $this->update($record['token'], $record['sequence']),
+ $this->update($record['token']),
$record['expiration']
);
}
@@ -238,17 +237,15 @@ class RememberMe extends Base
*
* @access public
* @param string $token Session token
- * @param string $sequence Sequence token
* @return string
*/
- public function update($token, $sequence)
+ public function update($token)
{
$new_sequence = Security::generateToken();
$this->db
->table(self::TABLE)
->eq('token', $token)
- ->eq('sequence', $sequence)
->update(array('sequence' => $new_sequence));
return $new_sequence;
@@ -311,7 +308,7 @@ class RememberMe extends Base
$expiration,
BASE_URL_DIRECTORY,
null,
- Tool::isHTTPS(),
+ Request::isHTTPS(),
true
);
}
@@ -344,7 +341,7 @@ class RememberMe extends Base
time() - 3600,
BASE_URL_DIRECTORY,
null,
- Tool::isHTTPS(),
+ Request::isHTTPS(),
true
);
}
diff --git a/app/Auth/ReverseProxy.php b/app/Auth/ReverseProxy.php
index 5aca881a..c8fd5eec 100644
--- a/app/Auth/ReverseProxy.php
+++ b/app/Auth/ReverseProxy.php
@@ -2,8 +2,7 @@
namespace Auth;
-use Core\Request;
-use Core\Security;
+use Event\AuthEvent;
/**
* ReverseProxy backend
@@ -33,21 +32,13 @@ class ReverseProxy extends Base
$login = $_SERVER[REVERSE_PROXY_USER_HEADER];
$user = $this->user->getByUsername($login);
- if (! $user) {
+ if (empty($user)) {
$this->createUser($login);
$user = $this->user->getByUsername($login);
}
- // Create the user session
- $this->user->updateSession($user);
-
- // Update login history
- $this->lastLogin->create(
- self::AUTH_NAME,
- $user['id'],
- Request::getIpAddress(),
- Request::getUserAgent()
- );
+ $this->userSession->refresh($user);
+ $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id']));
return true;
}
@@ -75,6 +66,7 @@ class ReverseProxy extends Base
'username' => $login,
'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login,
'is_ldap_user' => 1,
+ 'disable_login_form' => 1,
));
}
}
diff --git a/app/Console/Base.php b/app/Console/Base.php
new file mode 100644
index 00000000..07243080
--- /dev/null
+++ b/app/Console/Base.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Console;
+
+use Pimple\Container;
+use Symfony\Component\Console\Command\Command;
+
+/**
+ * Base command class
+ *
+ * @package console
+ * @author Frederic Guillot
+ *
+ * @property \Model\Notification $notification
+ * @property \Model\Project $project
+ * @property \Model\ProjectPermission $projectPermission
+ * @property \Model\ProjectAnalytic $projectAnalytic
+ * @property \Model\ProjectDailySummary $projectDailySummary
+ * @property \Model\SubtaskExport $subtaskExport
+ * @property \Model\Task $task
+ * @property \Model\TaskExport $taskExport
+ * @property \Model\TaskFinder $taskFinder
+ * @property \Model\Transition $transition
+ */
+abstract class Base extends Command
+{
+ /**
+ * Container instance
+ *
+ * @access protected
+ * @var \Pimple\Container
+ */
+ protected $container;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ */
+ public function __construct(Container $container)
+ {
+ parent::__construct();
+ $this->container = $container;
+ }
+
+ /**
+ * Load automatically models
+ *
+ * @access public
+ * @param string $name Model name
+ * @return mixed
+ */
+ public function __get($name)
+ {
+ return $this->container[$name];
+ }
+}
diff --git a/app/Console/ProjectDailySummaryCalculation.php b/app/Console/ProjectDailySummaryCalculation.php
new file mode 100644
index 00000000..b2ada1b6
--- /dev/null
+++ b/app/Console/ProjectDailySummaryCalculation.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Console;
+
+use Model\Project;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ProjectDailySummaryCalculation extends Base
+{
+ protected function configure()
+ {
+ $this
+ ->setName('projects:daily-summary')
+ ->setDescription('Calculate daily summary data for all projects');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $projects = $this->project->getAllByStatus(Project::ACTIVE);
+
+ foreach ($projects as $project) {
+ $output->writeln('Run calculation for '.$project['name']);
+ $this->projectDailySummary->updateTotals($project['id'], date('Y-m-d'));
+ }
+ }
+}
diff --git a/app/Console/ProjectDailySummaryExport.php b/app/Console/ProjectDailySummaryExport.php
new file mode 100644
index 00000000..07841d52
--- /dev/null
+++ b/app/Console/ProjectDailySummaryExport.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Console;
+
+use Core\Tool;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ProjectDailySummaryExport extends Base
+{
+ protected function configure()
+ {
+ $this
+ ->setName('export:daily-project-summary')
+ ->setDescription('Daily project summary CSV export (number of tasks per column and per day)')
+ ->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
+ ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
+ ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $data = $this->projectDailySummary->getAggregatedMetrics(
+ $input->getArgument('project_id'),
+ $input->getArgument('start_date'),
+ $input->getArgument('end_date')
+ );
+
+ if (is_array($data)) {
+ Tool::csv($data);
+ }
+ }
+}
diff --git a/app/Console/SubtaskExport.php b/app/Console/SubtaskExport.php
new file mode 100644
index 00000000..167a9225
--- /dev/null
+++ b/app/Console/SubtaskExport.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Console;
+
+use Core\Tool;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class SubtaskExport extends Base
+{
+ protected function configure()
+ {
+ $this
+ ->setName('export:subtasks')
+ ->setDescription('Subtasks CSV export')
+ ->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
+ ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
+ ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $data = $this->subtaskExport->export(
+ $input->getArgument('project_id'),
+ $input->getArgument('start_date'),
+ $input->getArgument('end_date')
+ );
+
+ if (is_array($data)) {
+ Tool::csv($data);
+ }
+ }
+}
diff --git a/app/Console/TaskExport.php b/app/Console/TaskExport.php
new file mode 100644
index 00000000..2ecd45e5
--- /dev/null
+++ b/app/Console/TaskExport.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Console;
+
+use Core\Tool;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class TaskExport extends Base
+{
+ protected function configure()
+ {
+ $this
+ ->setName('export:tasks')
+ ->setDescription('Tasks CSV export')
+ ->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
+ ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
+ ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $data = $this->taskExport->export(
+ $input->getArgument('project_id'),
+ $input->getArgument('start_date'),
+ $input->getArgument('end_date')
+ );
+
+ if (is_array($data)) {
+ Tool::csv($data);
+ }
+ }
+}
diff --git a/app/Console/TaskOverdueNotification.php b/app/Console/TaskOverdueNotification.php
new file mode 100644
index 00000000..3d254ae4
--- /dev/null
+++ b/app/Console/TaskOverdueNotification.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Console;
+
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class TaskOverdueNotification extends Base
+{
+ protected function configure()
+ {
+ $this
+ ->setName('notification:overdue-tasks')
+ ->setDescription('Send notifications for overdue tasks')
+ ->addOption('show', null, InputOption::VALUE_NONE, 'Show sent overdue tasks');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $tasks = $this->notification->sendOverdueTaskNotifications();
+
+ if ($input->getOption('show')) {
+ $this->showTable($output, $tasks);
+ }
+ }
+
+ public function showTable(OutputInterface $output, array $tasks)
+ {
+ $rows = array();
+
+ foreach ($tasks as $task) {
+ $rows[] = array(
+ $task['id'],
+ $task['title'],
+ date('Y-m-d', $task['date_due']),
+ $task['project_id'],
+ $task['project_name'],
+ $task['assignee_name'] ?: $task['assignee_username'],
+ );
+ }
+
+ $table = new Table($output);
+ $table
+ ->setHeaders(array('Id', 'Title', 'Due date', 'Project Id', 'Project name', 'Assignee'))
+ ->setRows($rows)
+ ->render();
+ }
+}
diff --git a/app/Console/TransitionExport.php b/app/Console/TransitionExport.php
new file mode 100644
index 00000000..ad988c54
--- /dev/null
+++ b/app/Console/TransitionExport.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Console;
+
+use Core\Tool;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class TransitionExport extends Base
+{
+ protected function configure()
+ {
+ $this
+ ->setName('export:transitions')
+ ->setDescription('Task transitions CSV export')
+ ->addArgument('project_id', InputArgument::REQUIRED, 'Project id')
+ ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)')
+ ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $data = $this->transition->export(
+ $input->getArgument('project_id'),
+ $input->getArgument('start_date'),
+ $input->getArgument('end_date')
+ );
+
+ if (is_array($data)) {
+ Tool::csv($data);
+ }
+ }
+}
diff --git a/app/Controller/Action.php b/app/Controller/Action.php
index 714c87f3..cd24453a 100644
--- a/app/Controller/Action.php
+++ b/app/Controller/Action.php
@@ -17,9 +17,9 @@ class Action extends Base
*/
public function index()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
- $this->response->html($this->projectLayout('action_index', array(
+ $this->response->html($this->projectLayout('action/index', array(
'values' => array('project_id' => $project['id']),
'project' => $project,
'actions' => $this->action->getAllByProject($project['id']),
@@ -27,11 +27,10 @@ class Action extends Base
'available_events' => $this->action->getAvailableEvents(),
'available_params' => $this->action->getAllActionParameters(),
'columns_list' => $this->board->getColumnsList($project['id']),
- 'users_list' => $this->projectPermission->getUsersList($project['id']),
+ 'users_list' => $this->projectPermission->getMemberList($project['id']),
'projects_list' => $this->project->getList(false),
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
- 'menu' => 'projects',
'title' => t('Automatic actions')
)));
}
@@ -43,18 +42,17 @@ class Action extends Base
*/
public function event()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$values = $this->request->getValues();
if (empty($values['action_name']) || empty($values['project_id'])) {
$this->response->redirect('?controller=action&action=index&project_id='.$project['id']);
}
- $this->response->html($this->projectLayout('action_event', array(
+ $this->response->html($this->projectLayout('action/event', array(
'values' => $values,
'project' => $project,
'events' => $this->action->getCompatibleEvents($values['action_name']),
- 'menu' => 'projects',
'title' => t('Automatic actions')
)));
}
@@ -66,7 +64,7 @@ class Action extends Base
*/
public function params()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$values = $this->request->getValues();
if (empty($values['action_name']) || empty($values['project_id']) || empty($values['event_name'])) {
@@ -83,16 +81,15 @@ class Action extends Base
$projects_list = $this->project->getList(false);
unset($projects_list[$project['id']]);
- $this->response->html($this->projectLayout('action_params', array(
+ $this->response->html($this->projectLayout('action/params', array(
'values' => $values,
'action_params' => $action_params,
'columns_list' => $this->board->getColumnsList($project['id']),
- 'users_list' => $this->projectPermission->getUsersList($project['id']),
+ 'users_list' => $this->projectPermission->getMemberList($project['id']),
'projects_list' => $projects_list,
'colors_list' => $this->color->getList(),
'categories_list' => $this->category->getList($project['id']),
'project' => $project,
- 'menu' => 'projects',
'title' => t('Automatic actions')
)));
}
@@ -104,7 +101,7 @@ class Action extends Base
*/
public function create()
{
- $this->doCreation($this->getProjectManagement(), $this->request->getValues());
+ $this->doCreation($this->getProject(), $this->request->getValues());
}
/**
@@ -138,14 +135,13 @@ class Action extends Base
*/
public function confirm()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
- $this->response->html($this->projectLayout('action_remove', array(
+ $this->response->html($this->projectLayout('action/remove', array(
'action' => $this->action->getById($this->request->getIntegerParam('action_id')),
'available_events' => $this->action->getAvailableEvents(),
'available_actions' => $this->action->getAvailableActions(),
'project' => $project,
- 'menu' => 'projects',
'title' => t('Remove an action')
)));
}
@@ -158,10 +154,10 @@ class Action extends Base
public function remove()
{
$this->checkCSRFParam();
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$action = $this->action->getById($this->request->getIntegerParam('action_id'));
- if ($action && $this->action->remove($action['id'])) {
+ if (! empty($action) && $this->action->remove($action['id'])) {
$this->session->flash(t('Action removed successfully.'));
} else {
$this->session->flashError(t('Unable to remove this action.'));
diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php
new file mode 100644
index 00000000..f31870e0
--- /dev/null
+++ b/app/Controller/Analytic.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Project Anaytic controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+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 tasks distribution graph
+ *
+ * @access public
+ */
+ public function tasks()
+ {
+ $project = $this->getProject();
+ $metrics = $this->projectAnalytic->getTaskRepartition($project['id']);
+
+ if ($this->request->isAjax()) {
+ $this->response->json(array(
+ 'metrics' => $metrics,
+ 'labels' => array(
+ 'column_title' => t('Column'),
+ 'nb_tasks' => t('Number of tasks'),
+ )
+ ));
+ }
+ else {
+ $this->response->html($this->layout('analytic/tasks', array(
+ 'project' => $project,
+ 'metrics' => $metrics,
+ 'title' => t('Task repartition for "%s"', $project['name']),
+ )));
+ }
+ }
+
+ /**
+ * Show users repartition
+ *
+ * @access public
+ */
+ public function users()
+ {
+ $project = $this->getProject();
+ $metrics = $this->projectAnalytic->getUserRepartition($project['id']);
+
+ if ($this->request->isAjax()) {
+ $this->response->json(array(
+ 'metrics' => $metrics,
+ 'labels' => array(
+ 'user' => t('User'),
+ 'nb_tasks' => t('Number of tasks'),
+ )
+ ));
+ }
+ else {
+ $this->response->html($this->layout('analytic/users', array(
+ 'project' => $project,
+ 'metrics' => $metrics,
+ 'title' => t('User repartition for "%s"', $project['name']),
+ )));
+ }
+ }
+
+ /**
+ * Show cumulative flow diagram
+ *
+ * @access public
+ */
+ public function cfd()
+ {
+ $project = $this->getProject();
+ $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'];
+ }
+
+ if ($this->request->isAjax()) {
+ $this->response->json(array(
+ 'columns' => array_values($this->board->getColumnsList($project['id'])),
+ 'metrics' => $this->projectDailySummary->getRawMetrics($project['id'], $from, $to),
+ 'labels' => array(
+ 'column' => t('Column'),
+ 'day' => t('Date'),
+ 'total' => t('Tasks'),
+ )
+ ));
+ }
+ else {
+ $this->response->html($this->layout('analytic/cfd', array(
+ 'values' => array(
+ 'from' => $from,
+ 'to' => $to,
+ ),
+ 'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2,
+ 'project' => $project,
+ 'date_format' => $this->config->get('application_date_format'),
+ 'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'title' => t('Cumulative flow diagram for "%s"', $project['name']),
+ )));
+ }
+ }
+
+ /**
+ * Show burndown chart
+ *
+ * @access public
+ */
+ public function burndown()
+ {
+ $project = $this->getProject();
+ $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'];
+ }
+
+ if ($this->request->isAjax()) {
+ $this->response->json(array(
+ 'metrics' => $this->projectDailySummary->getRawMetricsByDay($project['id'], $from, $to),
+ 'labels' => array(
+ 'day' => t('Date'),
+ 'score' => t('Complexity'),
+ )
+ ));
+ }
+ else {
+ $this->response->html($this->layout('analytic/burndown', array(
+ 'values' => array(
+ 'from' => $from,
+ 'to' => $to,
+ ),
+ 'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2,
+ 'project' => $project,
+ 'date_format' => $this->config->get('application_date_format'),
+ 'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'title' => t('Burndown chart for "%s"', $project['name']),
+ )));
+ }
+ }
+}
diff --git a/app/Controller/App.php b/app/Controller/App.php
index feec4221..8a97e8c7 100644
--- a/app/Controller/App.php
+++ b/app/Controller/App.php
@@ -2,7 +2,8 @@
namespace Controller;
-use Model\Project as ProjectModel;
+use Model\Subtask as SubtaskModel;
+use Model\Task as TaskModel;
/**
* Application controller
@@ -13,21 +14,117 @@ use Model\Project as ProjectModel;
class App extends Base
{
/**
+ * Check if the user is connected
+ *
+ * @access public
+ */
+ public function status()
+ {
+ $this->response->text('OK');
+ }
+
+ /**
+ * User dashboard view for admins
+ *
+ * @access public
+ */
+ public function dashboard()
+ {
+ $this->index($this->request->getIntegerParam('user_id'), 'dashboard');
+ }
+
+ /**
* Dashboard for the current user
*
* @access public
*/
- public function index()
+ public function index($user_id = 0, $action = 'index')
{
- $user_id = $this->acl->getUserId();
- $projects = $this->projectPermission->getAllowedProjects($user_id);
-
- $this->response->html($this->template->layout('app_index', array(
- 'board_selector' => $projects,
- 'events' => $this->projectActivity->getProjects(array_keys($projects), 10),
- 'tasks' => $this->taskFinder->getAllTasksByUser($user_id),
- 'menu' => 'dashboard',
+ $status = array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS);
+ $user_id = $user_id ?: $this->userSession->getId();
+ $projects = $this->projectPermission->getActiveMemberProjects($user_id);
+ $project_ids = array_keys($projects);
+
+ $task_paginator = $this->paginator
+ ->setUrl('app', $action, array('pagination' => 'tasks', 'user_id' => $user_id))
+ ->setMax(10)
+ ->setOrder('tasks.id')
+ ->setQuery($this->taskFinder->getUserQuery($user_id))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks');
+
+ $subtask_paginator = $this->paginator
+ ->setUrl('app', $action, array('pagination' => 'subtasks', 'user_id' => $user_id))
+ ->setMax(10)
+ ->setOrder('tasks.id')
+ ->setQuery($this->subtask->getUserQuery($user_id, $status))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks');
+
+ $project_paginator = $this->paginator
+ ->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id))
+ ->setMax(10)
+ ->setOrder('name')
+ ->setQuery($this->project->getQueryColumnStats($project_ids))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects');
+
+ $this->response->html($this->template->layout('app/dashboard', array(
'title' => t('Dashboard'),
+ 'board_selector' => $this->projectPermission->getAllowedProjects($user_id),
+ 'events' => $this->projectActivity->getProjects($project_ids, 5),
+ 'task_paginator' => $task_paginator,
+ 'subtask_paginator' => $subtask_paginator,
+ 'project_paginator' => $project_paginator,
+ 'user_id' => $user_id,
)));
}
+
+ /**
+ * 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']));
+ }
+
+ /**
+ * Colors stylesheet
+ *
+ * @access public
+ */
+ public function colors()
+ {
+ $this->response->css($this->color->getCss());
+ }
+
+ /**
+ * Task autocompletion (Ajax)
+ *
+ * @access public
+ */
+ public function autocomplete()
+ {
+ $search = $this->request->getStringParam('term');
+
+ $filter = $this->taskFilter
+ ->create()
+ ->filterByProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId()))
+ ->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->toAutoCompletion());
+ }
}
diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php
new file mode 100644
index 00000000..24e6e242
--- /dev/null
+++ b/app/Controller/Auth.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Authentication controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Auth extends Base
+{
+ /**
+ * Display the form login
+ *
+ * @access public
+ */
+ public function login(array $values = array(), array $errors = array())
+ {
+ if ($this->userSession->isLogged()) {
+ $this->response->redirect($this->helper->url->to('app', 'index'));
+ }
+
+ $this->response->html($this->template->layout('auth/index', array(
+ 'errors' => $errors,
+ 'values' => $values,
+ 'no_layout' => true,
+ 'redirect_query' => $this->request->getStringParam('redirect_query'),
+ 'title' => t('Login')
+ )));
+ }
+
+ /**
+ * Check credentials
+ *
+ * @access public
+ */
+ public function check()
+ {
+ $redirect_query = $this->request->getStringParam('redirect_query');
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->authentication->validateForm($values);
+
+ if ($valid) {
+
+ if ($redirect_query !== '') {
+ $this->response->redirect('?'.urldecode($redirect_query));
+ }
+
+ $this->response->redirect($this->helper->url->to('app', 'index'));
+ }
+
+ $this->login($values, $errors);
+ }
+
+ /**
+ * Logout and destroy session
+ *
+ * @access public
+ */
+ public function logout()
+ {
+ $this->authentication->backend('rememberMe')->destroy($this->userSession->getId());
+ $this->session->close();
+ $this->response->redirect($this->helper->url->to('auth', 'login'));
+ }
+}
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index a8e22fd8..fcd07b99 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -2,166 +2,171 @@
namespace Controller;
-use Core\Tool;
-use Core\Registry;
+use Pimple\Container;
use Core\Security;
+use Core\Request;
+use Core\Response;
+use Core\Template;
+use Core\Session;
use Model\LastLogin;
+use Symfony\Component\EventDispatcher\Event;
/**
* Base controller
*
* @package controller
* @author Frederic Guillot
- *
- * @property \Model\Acl $acl
- * @property \Model\Authentication $authentication
- * @property \Model\Action $action
- * @property \Model\Board $board
- * @property \Model\Category $category
- * @property \Model\Color $color
- * @property \Model\Comment $comment
- * @property \Model\Config $config
- * @property \Model\File $file
- * @property \Model\LastLogin $lastLogin
- * @property \Model\Notification $notification
- * @property \Model\Project $project
- * @property \Model\ProjectPermission $projectPermission
- * @property \Model\SubTask $subTask
- * @property \Model\Task $task
- * @property \Model\TaskHistory $taskHistory
- * @property \Model\TaskExport $taskExport
- * @property \Model\TaskFinder $taskFinder
- * @property \Model\TaskPermission $taskPermission
- * @property \Model\TaskValidator $taskValidator
- * @property \Model\CommentHistory $commentHistory
- * @property \Model\SubtaskHistory $subtaskHistory
- * @property \Model\TimeTracking $timeTracking
- * @property \Model\User $user
- * @property \Model\Webhook $webhook
*/
-abstract class Base
+abstract class Base extends \Core\Base
{
/**
* Request instance
*
- * @accesss public
+ * @accesss protected
* @var \Core\Request
*/
- public $request;
+ protected $request;
/**
* Response instance
*
- * @accesss public
+ * @accesss protected
* @var \Core\Response
*/
- public $response;
-
- /**
- * Template instance
- *
- * @accesss public
- * @var \Core\Template
- */
- public $template;
-
- /**
- * Session instance
- *
- * @accesss public
- * @var \Core\Session
- */
- public $session;
-
- /**
- * Registry instance
- *
- * @access private
- * @var \Core\Registry
- */
- private $registry;
+ protected $response;
/**
* Constructor
*
* @access public
- * @param \Core\Registry $registry Registry instance
+ * @param \Pimple\Container $container
*/
- public function __construct(Registry $registry)
+ public function __construct(Container $container)
{
- $this->registry = $registry;
+ $this->container = $container;
+ $this->request = new Request;
+ $this->response = new Response;
+
+ if (DEBUG) {
+ $this->container['logger']->debug('START_REQUEST='.$_SERVER['REQUEST_URI']);
+ }
}
/**
- * Load automatically models
+ * Destructor
*
* @access public
- * @param string $name Model name
- * @return mixed
*/
- public function __get($name)
+ public function __destruct()
{
- return Tool::loadModel($this->registry, $name);
+ if (DEBUG) {
+
+ foreach ($this->container['db']->getLogMessages() as $message) {
+ $this->container['logger']->debug($message);
+ }
+
+ $this->container['logger']->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nb_queries));
+ $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT']));
+ $this->container['logger']->debug('END_REQUEST='.$_SERVER['REQUEST_URI']);
+ }
}
/**
- * Method executed before each action
+ * Send HTTP headers
*
- * @access public
+ * @access private
*/
- public function beforeAction($controller, $action)
+ private function sendHeaders($action)
{
- // Start the session
- $this->session->open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH);
-
// HTTP secure headers
- $this->response->csp(array('style-src' => "'self' 'unsafe-inline'"));
+ $this->response->csp(array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '*'));
$this->response->nosniff();
$this->response->xss();
// Allow the public board iframe inclusion
- if ($action !== 'readonly') {
+ if (ENABLE_XFRAME && $action !== 'readonly') {
$this->response->xframe();
}
if (ENABLE_HSTS) {
$this->response->hsts();
}
+ }
+
+ /**
+ * Method executed before each action
+ *
+ * @access public
+ */
+ public function beforeAction($controller, $action)
+ {
+ // Start the session
+ $this->session->open(BASE_URL_DIRECTORY);
+ $this->sendHeaders($action);
+ $this->container['dispatcher']->dispatch('session.bootstrap', new Event);
- $this->config->setupTranslations();
- $this->config->setupTimezone();
+ if (! $this->acl->isPublicAction($controller, $action)) {
+ $this->handleAuthentication();
+ $this->handle2FA($controller, $action);
+ $this->handleAuthorization($controller, $action);
- // Authentication
- if (! $this->authentication->isAuthenticated($controller, $action)) {
- $this->response->redirect('?controller=user&action=login&redirect_query='.urlencode($this->request->getQueryString()));
+ $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
}
+ }
- // Check if the user is allowed to see this page
- if (! $this->acl->isPageAccessAllowed($controller, $action)) {
- $this->response->redirect('?controller=user&action=forbidden');
+ /**
+ * Check authentication
+ *
+ * @access public
+ */
+ public function handleAuthentication()
+ {
+ if (! $this->authentication->isAuthenticated()) {
+
+ if ($this->request->isAjax()) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ $this->response->redirect($this->helper->url->to('auth', 'login', array('redirect_query' => urlencode($this->request->getQueryString()))));
}
+ }
- // Attach events
- $this->attachEvents();
+ /**
+ * Check 2FA
+ *
+ * @access public
+ */
+ public function handle2FA($controller, $action)
+ {
+ $ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout');
+
+ if ($ignore === false && $this->userSession->has2FA() && ! $this->userSession->check2FA()) {
+
+ if ($this->request->isAjax()) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ $this->response->redirect($this->helper->url->to('twofactor', 'code'));
+ }
}
/**
- * Attach events
+ * Check page access and authorization
*
- * @access private
+ * @access public
*/
- private function attachEvents()
+ public function handleAuthorization($controller, $action)
{
- $models = array(
- 'projectActivity', // Order is important
- 'action',
- 'project',
- 'webhook',
- 'notification',
- );
-
- foreach ($models as $model) {
- $this->$model->attachEvents();
+ $project_id = $this->request->getIntegerParam('project_id');
+ $task_id = $this->request->getIntegerParam('task_id');
+
+ // Allow urls without "project_id"
+ if ($task_id > 0 && $project_id === 0) {
+ $project_id = $this->taskFinder->getProjectId($task_id);
+ }
+
+ if (! $this->acl->isAllowed($controller, $action, $project_id)) {
+ $this->forbidden();
}
}
@@ -173,7 +178,7 @@ abstract class Base
*/
public function notfound($no_layout = false)
{
- $this->response->html($this->template->layout('app_notfound', array(
+ $this->response->html($this->template->layout('app/notfound', array(
'title' => t('Page not found'),
'no_layout' => $no_layout,
)));
@@ -187,7 +192,7 @@ abstract class Base
*/
public function forbidden($no_layout = false)
{
- $this->response->html($this->template->layout('app_forbidden', array(
+ $this->response->html($this->template->layout('app/forbidden', array(
'title' => t('Access Forbidden'),
'no_layout' => $no_layout,
)));
@@ -206,19 +211,6 @@ abstract class Base
}
/**
- * Check if the current user have access to the given project
- *
- * @access protected
- * @param integer $project_id Project id
- */
- protected function checkProjectPermissions($project_id)
- {
- if ($this->acl->isRegularUser() && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) {
- $this->forbidden();
- }
- }
-
- /**
* Redirection when there is no project in the database
*
* @access protected
@@ -239,14 +231,12 @@ abstract class Base
*/
protected function taskLayout($template, array $params)
{
- if (isset($params['task']) && $this->taskPermission->canRemoveTask($params['task']) === false) {
- $params['hide_remove_menu'] = true;
- }
-
- $content = $this->template->load($template, $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);
+ return $this->template->layout('task/layout', $params);
}
/**
@@ -257,13 +247,15 @@ abstract class Base
* @param array $params Template parameters
* @return string
*/
- protected function projectLayout($template, array $params)
+ protected function projectLayout($template, array $params, $sidebar_template = 'project/sidebar')
{
- $content = $this->template->load($template, $params);
+ $content = $this->template->render($template, $params);
$params['project_content_for_layout'] = $content;
- $params['menu'] = 'projects';
+ $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);
+ return $this->template->layout('project/layout', $params);
}
/**
@@ -276,12 +268,10 @@ abstract class Base
{
$task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id'));
- if (! $task) {
+ if (empty($task)) {
$this->notfound();
}
- $this->checkProjectPermissions($task['project_id']);
-
return $task;
}
@@ -297,34 +287,11 @@ abstract class Base
$project_id = $this->request->getIntegerParam('project_id', $project_id);
$project = $this->project->getById($project_id);
- if (! $project) {
+ if (empty($project)) {
$this->session->flashError(t('Project not found.'));
$this->response->redirect('?controller=project');
}
- $this->checkProjectPermissions($project['id']);
-
- return $project;
- }
-
- /**
- * Common method to get a project with administration rights
- *
- * @access protected
- * @return array
- */
- protected function getProjectManagement()
- {
- $project = $this->project->getById($this->request->getIntegerParam('project_id'));
-
- if (! $project) {
- $this->notfound();
- }
-
- if ($this->acl->isRegularUser() && ! $this->projectPermission->adminAllowed($project['id'], $this->acl->getUserId())) {
- $this->forbidden();
- }
-
return $project;
}
}
diff --git a/app/Controller/Board.php b/app/Controller/Board.php
index d49ad021..2b633d82 100644
--- a/app/Controller/Board.php
+++ b/app/Controller/Board.php
@@ -2,10 +2,6 @@
namespace Controller;
-use Model\Project as ProjectModel;
-use Model\User as UserModel;
-use Core\Security;
-
/**
* Board controller
*
@@ -15,133 +11,6 @@ use Core\Security;
class Board extends Base
{
/**
- * Move a column down or up
- *
- * @access public
- */
- public function moveColumn()
- {
- $this->checkCSRFParam();
- $project = $this->getProjectManagement();
- $column_id = $this->request->getIntegerParam('column_id');
- $direction = $this->request->getStringParam('direction');
-
- if ($direction === 'up' || $direction === 'down') {
- $this->board->{'move'.$direction}($project['id'], $column_id);
- }
-
- $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
- }
-
- /**
- * Change a task assignee directly from the board
- *
- * @access public
- */
- public function changeAssignee()
- {
- $task = $this->getTask();
- $project = $this->project->getById($task['project_id']);
- $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
- $params = array(
- 'errors' => array(),
- 'values' => $task,
- 'users_list' => $this->projectPermission->getUsersList($project['id']),
- 'projects' => $projects,
- 'current_project_id' => $project['id'],
- 'current_project_name' => $project['name'],
- );
-
- if ($this->request->isAjax()) {
-
- $this->response->html($this->template->load('board_assignee', $params));
- }
- else {
-
- $this->response->html($this->template->layout('board_assignee', $params + array(
- 'menu' => 'boards',
- 'title' => t('Change assignee').' - '.$task['title'],
- )));
- }
- }
-
- /**
- * Validate an assignee modification
- *
- * @access public
- */
- public function updateAssignee()
- {
- $values = $this->request->getValues();
- $this->checkProjectPermissions($values['project_id']);
-
- list($valid,) = $this->taskValidator->validateAssigneeModification($values);
-
- if ($valid && $this->task->update($values)) {
- $this->session->flash(t('Task updated successfully.'));
- }
- else {
- $this->session->flashError(t('Unable to update your task.'));
- }
-
- $this->response->redirect('?controller=board&action=show&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']);
- $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
- $params = array(
- 'errors' => array(),
- 'values' => $task,
- 'categories_list' => $this->category->getList($project['id']),
- 'projects' => $projects,
- 'current_project_id' => $project['id'],
- 'current_project_name' => $project['name'],
- );
-
- if ($this->request->isAjax()) {
-
- $this->response->html($this->template->load('board_category', $params));
- }
- else {
-
- $this->response->html($this->template->layout('board_category', $params + array(
- 'menu' => 'boards',
- 'title' => t('Change category').' - '.$task['title'],
- )));
- }
- }
-
- /**
- * Validate a category modification
- *
- * @access public
- */
- public function updateCategory()
- {
- $values = $this->request->getValues();
- $this->checkProjectPermissions($values['project_id']);
-
- list($valid,) = $this->taskValidator->validateCategoryModification($values);
-
- if ($valid && $this->task->update($values)) {
- $this->session->flash(t('Task updated successfully.'));
- }
- else {
- $this->session->flashError(t('Unable to update your task.'));
- }
-
- $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']);
- }
-
- /**
* Display the public version of a board
* Access checked by a simple token, no user login, read only, auto-refresh
*
@@ -153,19 +22,25 @@ class Board extends Base
$project = $this->project->getByToken($token);
// Token verification
- if (! $project) {
+ if (empty($project)) {
$this->forbidden(true);
}
+ list($categories_listing, $categories_description) = $this->category->getBoardCategories($project['id']);
+
// Display the board with a specific layout
- $this->response->html($this->template->layout('board_public', array(
+ $this->response->html($this->template->layout('board/public', array(
'project' => $project,
- 'columns' => $this->board->get($project['id']),
- 'categories' => $this->category->getList($project['id'], false),
+ 'swimlanes' => $this->board->getBoard($project['id']),
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
'title' => $project['name'],
+ 'description' => $project['description'],
'no_layout' => true,
'not_editable' => true,
'board_public_refresh_interval' => $this->config->get('board_public_refresh_interval'),
+ 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
+ 'board_highlight_period' => $this->config->get('board_highlight_period'),
)));
}
@@ -176,16 +51,16 @@ class Board extends Base
*/
public function index()
{
- $last_seen_project_id = $this->user->getLastSeenProjectId();
- $favorite_project_id = $this->user->getFavoriteProjectId();
+ $last_seen_project_id = $this->userSession->getLastSeenProjectId();
+ $favorite_project_id = $this->userSession->getFavoriteProjectId();
$project_id = $last_seen_project_id ?: $favorite_project_id;
if (! $project_id) {
- $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
+ $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId());
if (empty($projects)) {
- if ($this->acl->isAdminUser()) {
+ if ($this->userSession->isAdmin()) {
$this->redirectNoProject();
}
@@ -207,23 +82,24 @@ class Board extends Base
public function show($project_id = 0)
{
$project = $this->getProject($project_id);
- $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
+ $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$board_selector = $projects;
unset($board_selector[$project['id']]);
- $this->user->storeLastSeenProjectId($project['id']);
+ $this->userSession->storeLastSeenProjectId($project['id']);
+
+ list($categories_listing, $categories_description) = $this->category->getBoardCategories($project['id']);
- $this->response->html($this->template->layout('board_index', array(
- 'users' => $this->projectPermission->getUsersList($project['id'], true, true),
- 'filters' => array('user_id' => UserModel::EVERYBODY_ID),
+ $this->response->html($this->template->layout('board/index', array(
+ 'users' => $this->projectPermission->getMemberList($project['id'], true, true),
'projects' => $projects,
- 'current_project_id' => $project['id'],
- 'current_project_name' => $project['name'],
- 'board' => $this->board->get($project['id']),
- 'categories' => $this->category->getList($project['id'], true, true),
- 'menu' => 'boards',
+ 'project' => $project,
+ 'swimlanes' => $this->board->getBoard($project['id']),
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
'title' => $project['name'],
+ 'description' => $project['description'],
'board_selector' => $board_selector,
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
@@ -231,215 +107,264 @@ class Board extends Base
}
/**
- * Display a form to edit a board
+ * Save the board (Ajax request made by the drag and drop)
*
* @access public
*/
- public function edit()
+ public function save()
{
- $project = $this->getProjectManagement();
- $columns = $this->board->getColumns($project['id']);
- $values = array();
+ $project_id = $this->request->getIntegerParam('project_id');
- foreach ($columns as $column) {
- $values['title['.$column['id'].']'] = $column['title'];
- $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null;
+ if (! $project_id || ! $this->request->isAjax()) {
+ return $this->response->status(403);
}
- $this->response->html($this->projectLayout('board_edit', array(
- 'errors' => array(),
- 'values' => $values + array('project_id' => $project['id']),
- 'columns' => $columns,
- 'project' => $project,
- 'menu' => 'projects',
- 'title' => t('Edit board')
- )));
+ if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
+ $this->response->text('Forbidden', 403);
+ }
+
+ $values = $this->request->getJson();
+
+ $result =$this->taskPosition->movePosition(
+ $project_id,
+ $values['task_id'],
+ $values['column_id'],
+ $values['position'],
+ $values['swimlane_id']
+ );
+
+ if (! $result) {
+ return $this->response->status(400);
+ }
+
+ list($categories_listing, $categories_description) = $this->category->getBoardCategories($project_id);
+
+ $this->response->html(
+ $this->template->render('board/show', array(
+ 'project' => $this->project->getById($project_id),
+ 'swimlanes' => $this->board->getBoard($project_id),
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
+ 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
+ 'board_highlight_period' => $this->config->get('board_highlight_period'),
+ )),
+ 201
+ );
}
/**
- * Validate and update a board
+ * Check if the board have been changed
*
* @access public
*/
- public function update()
+ public function check()
{
- $project = $this->getProjectManagement();
- $columns = $this->board->getColumns($project['id']);
- $data = $this->request->getValues();
- $values = $columns_list = array();
-
- foreach ($columns as $column) {
- $columns_list[$column['id']] = $column['title'];
- $values['title['.$column['id'].']'] = isset($data['title'][$column['id']]) ? $data['title'][$column['id']] : '';
- $values['task_limit['.$column['id'].']'] = isset($data['task_limit'][$column['id']]) ? $data['task_limit'][$column['id']] : 0;
+ if (! $this->request->isAjax()) {
+ return $this->response->status(403);
}
- list($valid, $errors) = $this->board->validateModification($columns_list, $values);
+ $project_id = $this->request->getIntegerParam('project_id');
+ $timestamp = $this->request->getIntegerParam('timestamp');
- if ($valid) {
+ if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) {
+ $this->response->text('Forbidden', 403);
+ }
- if ($this->board->update($data)) {
- $this->session->flash(t('Board updated successfully.'));
- $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
- }
- else {
- $this->session->flashError(t('Unable to update this board.'));
- }
+ if (! $this->project->isModifiedSince($project_id, $timestamp)) {
+ return $this->response->status(304);
}
- $this->response->html($this->projectLayout('board_edit', array(
- 'errors' => $errors,
- 'values' => $values + array('project_id' => $project['id']),
- 'columns' => $columns,
- 'project' => $project,
- 'menu' => 'projects',
- 'title' => t('Edit board')
- )));
+ list($categories_listing, $categories_description) = $this->category->getBoardCategories($project_id);
+
+ $this->response->html(
+ $this->template->render('board/show', array(
+ 'project' => $this->project->getById($project_id),
+ 'swimlanes' => $this->board->getBoard($project_id),
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
+ 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
+ 'board_highlight_period' => $this->config->get('board_highlight_period'),
+ ))
+ );
}
/**
- * Validate and add a new column
+ * Get links on mouseover
*
* @access public
*/
- public function add()
+ public function tasklinks()
{
- $project = $this->getProjectManagement();
- $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;
- }
-
- list($valid, $errors) = $this->board->validateCreation($data);
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/tasklinks', array(
+ 'links' => $this->taskLink->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
- if ($valid) {
+ /**
+ * Get subtasks on mouseover
+ *
+ * @access public
+ */
+ public function subtasks()
+ {
+ $task = $this->getTask();
+ $this->response->html($this->template->render('board/subtasks', array(
+ 'subtasks' => $this->subtask->getAll($task['id']),
+ 'task' => $task,
+ )));
+ }
- if ($this->board->addColumn($project['id'], $data['title'])) {
- $this->session->flash(t('Board updated successfully.'));
- $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
- }
- else {
- $this->session->flashError(t('Unable to update this board.'));
- }
- }
+ /**
+ * Display all attachments during the task mouseover
+ *
+ * @access public
+ */
+ public function attachments()
+ {
+ $task = $this->getTask();
- $this->response->html($this->projectLayout('board_edit', array(
- 'errors' => $errors,
- 'values' => $values + $data,
- 'columns' => $columns,
- 'project' => $project,
- 'menu' => 'projects',
- 'title' => t('Edit board')
+ $this->response->html($this->template->render('board/files', array(
+ 'files' => $this->file->getAllDocuments($task['id']),
+ 'images' => $this->file->getAllImages($task['id']),
+ 'task' => $task,
)));
}
/**
- * Remove a column
+ * Display comments during a task mouseover
*
* @access public
*/
- public function remove()
+ public function comments()
{
- $project = $this->getProjectManagement();
+ $task = $this->getTask();
- if ($this->request->getStringParam('remove') === 'yes') {
+ $this->response->html($this->template->render('board/comments', array(
+ 'comments' => $this->comment->getAll($task['id'])
+ )));
+ }
- $this->checkCSRFParam();
- $column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
+ /**
+ * Display task description
+ *
+ * @access public
+ */
+ public function description()
+ {
+ $task = $this->getTask();
- if ($column && $this->board->removeColumn($column['id'])) {
- $this->session->flash(t('Column removed successfully.'));
- } else {
- $this->session->flashError(t('Unable to remove this column.'));
- }
+ $this->response->html($this->template->render('board/description', array(
+ 'task' => $task
+ )));
+ }
- $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']);
- }
+ /**
+ * 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->projectLayout('board_remove', array(
- 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
+ $this->response->html($this->template->render('board/assignee', array(
+ 'values' => $task,
+ 'users_list' => $this->projectPermission->getMemberList($project['id']),
'project' => $project,
- 'menu' => 'projects',
- 'title' => t('Remove a column from a board')
)));
}
/**
- * Save the board (Ajax request made by the drag and drop)
+ * Validate an assignee modification
*
* @access public
*/
- public function save()
+ public function updateAssignee()
{
- $project_id = $this->request->getIntegerParam('project_id');
+ $values = $this->request->getValues();
- if ($project_id > 0 && $this->request->isAjax()) {
+ list($valid,) = $this->taskValidator->validateAssigneeModification($values);
- if (! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) {
- $this->response->status(401);
- }
+ if ($valid && $this->taskModification->update($values)) {
+ $this->session->flash(t('Task updated successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to update your task.'));
+ }
- $values = $this->request->getValues();
+ $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
+ }
- if ($this->task->movePosition($project_id, $values['task_id'], $values['column_id'], $values['position'])) {
+ /**
+ * 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->load('board_show', array(
- 'current_project_id' => $project_id,
- 'board' => $this->board->get($project_id),
- 'categories' => $this->category->getList($project_id, false),
- 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
- 'board_highlight_period' => $this->config->get('board_highlight_period'),
- )),
- 201
- );
- }
- else {
+ $this->response->html($this->template->render('board/category', array(
+ 'values' => $task,
+ 'categories_list' => $this->category->getList($project['id']),
+ 'project' => $project,
+ )));
+ }
- $this->response->status(400);
- }
+ /**
+ * 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->response->status(401);
+ $this->session->flashError(t('Unable to update your task.'));
}
+
+ $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id'])));
}
/**
- * Check if the board have been changed
+ * Screenshot popover
*
* @access public
*/
- public function check()
+ public function screenshot()
{
- if ($this->request->isAjax()) {
+ $task = $this->getTask();
- $project_id = $this->request->getIntegerParam('project_id');
- $timestamp = $this->request->getIntegerParam('timestamp');
+ $this->response->html($this->template->render('file/screenshot', array(
+ 'task' => $task,
+ 'redirect' => 'board',
+ )));
+ }
- if ($project_id > 0 && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) {
- $this->response->text('Not Authorized', 401);
- }
+ /**
+ * Get recurrence information on mouseover
+ *
+ * @access public
+ */
+ public function recurrence()
+ {
+ $task = $this->getTask();
- if ($this->project->isModifiedSince($project_id, $timestamp)) {
- $this->response->html(
- $this->template->load('board_show', array(
- 'current_project_id' => $project_id,
- 'board' => $this->board->get($project_id),
- 'categories' => $this->category->getList($project_id, false),
- 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
- 'board_highlight_period' => $this->config->get('board_highlight_period'),
- ))
- );
- }
- else {
- $this->response->status(304);
- }
- }
- else {
- $this->response->status(401);
- }
+ $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(),
+ )));
}
}
diff --git a/app/Controller/Budget.php b/app/Controller/Budget.php
new file mode 100644
index 00000000..a2f7e0db
--- /dev/null
+++ b/app/Controller/Budget.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Budget
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Budget extends Base
+{
+ /**
+ * Budget index page
+ *
+ * @access public
+ */
+ public function index()
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->projectLayout('budget/index', array(
+ 'daily_budget' => $this->budget->getDailyBudgetBreakdown($project['id']),
+ 'project' => $project,
+ 'title' => t('Budget')
+ ), 'budget/sidebar'));
+ }
+
+ /**
+ * Cost breakdown by users/subtasks/tasks
+ *
+ * @access public
+ */
+ public function breakdown()
+ {
+ $project = $this->getProject();
+
+ $paginator = $this->paginator
+ ->setUrl('budget', 'breakdown', array('project_id' => $project['id']))
+ ->setMax(30)
+ ->setOrder('start')
+ ->setDirection('DESC')
+ ->setQuery($this->budget->getSubtaskBreakdown($project['id']))
+ ->calculate();
+
+ $this->response->html($this->projectLayout('budget/breakdown', array(
+ 'paginator' => $paginator,
+ 'project' => $project,
+ 'title' => t('Budget')
+ ), 'budget/sidebar'));
+ }
+
+ /**
+ * Create budget lines
+ *
+ * @access public
+ */
+ public function create(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+
+ if (empty($values)) {
+ $values['date'] = date('Y-m-d');
+ }
+
+ $this->response->html($this->projectLayout('budget/create', array(
+ 'lines' => $this->budget->getAll($project['id']),
+ 'values' => $values + array('project_id' => $project['id']),
+ 'errors' => $errors,
+ 'project' => $project,
+ 'title' => t('Budget lines')
+ ), 'budget/sidebar'));
+ }
+
+ /**
+ * Validate and save a new budget
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $project = $this->getProject();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->budget->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->budget->create($values['project_id'], $values['amount'], $values['comment'], $values['date'])) {
+ $this->session->flash(t('The budget line have been created successfully.'));
+ $this->response->redirect($this->helper->url->to('budget', 'create', array('project_id' => $project['id'])));
+ }
+ else {
+ $this->session->flashError(t('Unable to create the budget line.'));
+ }
+ }
+
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a budget
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->projectLayout('budget/remove', array(
+ 'project' => $project,
+ 'budget_id' => $this->request->getIntegerParam('budget_id'),
+ 'title' => t('Remove a budget line'),
+ ), 'budget/sidebar'));
+ }
+
+ /**
+ * Remove a budget
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+
+ if ($this->budget->remove($this->request->getIntegerParam('budget_id'))) {
+ $this->session->flash(t('Budget line removed successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to remove this budget line.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('budget', 'create', array('project_id' => $project['id'])));
+ }
+}
diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php
new file mode 100644
index 00000000..41642a59
--- /dev/null
+++ b/app/Controller/Calendar.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Controller;
+
+use Model\Task as TaskModel;
+
+/**
+ * Project Calendar controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ * @author Timo Litzbarski
+ */
+class Calendar extends Base
+{
+ /**
+ * Show calendar view for projects
+ *
+ * @access public
+ */
+ public function show()
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->template->layout('calendar/show', array(
+ 'check_interval' => $this->config->get('board_private_refresh_interval'),
+ 'users_list' => $this->projectPermission->getMemberList($project['id'], true, true),
+ 'categories_list' => $this->category->getList($project['id'], true, true),
+ 'columns_list' => $this->board->getColumnsList($project['id'], true),
+ 'swimlanes_list' => $this->swimlane->getList($project['id'], true),
+ 'colors_list' => $this->color->getList(true),
+ 'status_list' => $this->taskStatus->getList(true),
+ 'project' => $project,
+ 'title' => t('Calendar for "%s"', $project['name']),
+ 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ )));
+ }
+
+ /**
+ * Get tasks to display on the calendar (project view)
+ *
+ * @access public
+ */
+ public function project()
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+ $start = $this->request->getStringParam('start');
+ $end = $this->request->getStringParam('end');
+
+ // Common filter
+ $filter = $this->taskFilter
+ ->create()
+ ->filterByProject($project_id)
+ ->filterByCategory($this->request->getIntegerParam('category_id', -1))
+ ->filterByOwner($this->request->getIntegerParam('owner_id', -1))
+ ->filterByColumn($this->request->getIntegerParam('column_id', -1))
+ ->filterBySwimlane($this->request->getIntegerParam('swimlane_id', -1))
+ ->filterByColor($this->request->getStringParam('color_id'))
+ ->filterByStatus($this->request->getIntegerParam('is_active', -1));
+
+ // Tasks
+ if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
+ $events = $filter->copy()->filterByCreationDateRange($start, $end)->toDateTimeCalendarEvents('date_creation', 'date_completed');
+ }
+ else {
+ $events = $filter->copy()->filterByStartDateRange($start, $end)->toDateTimeCalendarEvents('date_started', 'date_completed');
+ }
+
+ // Tasks with due date
+ $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->toAllDayCalendarEvents());
+
+ $this->response->json($events);
+ }
+
+ /**
+ * Get tasks to display on the calendar (user view)
+ *
+ * @access public
+ */
+ public function user()
+ {
+ $user_id = $this->request->getIntegerParam('user_id');
+ $start = $this->request->getStringParam('start');
+ $end = $this->request->getStringParam('end');
+ $filter = $this->taskFilter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN);
+
+ // Task with due date
+ $events = $filter->copy()->filterByDueDateRange($start, $end)->toAllDayCalendarEvents();
+
+ // Tasks
+ if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') {
+ $events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->toDateTimeCalendarEvents('date_creation', 'date_completed'));
+ }
+ else {
+ $events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->toDateTimeCalendarEvents('date_started', 'date_completed'));
+ }
+
+ // Subtasks time tracking
+ if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) {
+ $events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end));
+ }
+
+ // Subtask estimates
+ if ($this->config->get('calendar_user_subtasks_forecast') == 1) {
+ $events = array_merge($events, $this->subtaskForecast->getCalendarEvents($user_id, $end));
+ }
+
+ $this->response->json($events);
+ }
+
+ /**
+ * Update task due date
+ *
+ * @access public
+ */
+ public function save()
+ {
+ if ($this->request->isAjax() && $this->request->isPost()) {
+
+ $values = $this->request->getJson();
+
+ $this->taskModification->update(array(
+ 'id' => $values['task_id'],
+ 'date_due' => substr($values['date_due'], 0, 10),
+ ));
+ }
+ }
+}
diff --git a/app/Controller/Category.php b/app/Controller/Category.php
index 38322294..515cc9c8 100644
--- a/app/Controller/Category.php
+++ b/app/Controller/Category.php
@@ -14,14 +14,14 @@ class Category extends Base
* Get the category (common method between actions)
*
* @access private
- * @param $project_id
+ * @param integer $project_id
* @return array
*/
private function getCategory($project_id)
{
$category = $this->category->getById($this->request->getIntegerParam('category_id'));
- if (! $category) {
+ if (empty($category)) {
$this->session->flashError(t('Category not found.'));
$this->response->redirect('?controller=category&action=index&project_id='.$project_id);
}
@@ -34,28 +34,27 @@ class Category extends Base
*
* @access public
*/
- public function index()
+ public function index(array $values = array(), array $errors = array())
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
- $this->response->html($this->projectLayout('category_index', array(
+ $this->response->html($this->projectLayout('category/index', array(
'categories' => $this->category->getList($project['id'], false),
- 'values' => array('project_id' => $project['id']),
- 'errors' => array(),
+ 'values' => $values + array('project_id' => $project['id']),
+ 'errors' => $errors,
'project' => $project,
- 'menu' => 'projects',
'title' => t('Categories')
)));
}
/**
- * Validate and save a new project
+ * Validate and save a new category
*
* @access public
*/
public function save()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->category->validateCreation($values);
@@ -71,14 +70,7 @@ class Category extends Base
}
}
- $this->response->html($this->projectLayout('category_index', array(
- 'categories' => $this->category->getList($project['id'], false),
- 'values' => $values,
- 'errors' => $errors,
- 'project' => $project,
- 'menu' => 'projects',
- 'title' => t('Categories')
- )));
+ $this->index($values, $errors);
}
/**
@@ -86,16 +78,15 @@ class Category extends Base
*
* @access public
*/
- public function edit()
+ public function edit(array $values = array(), array $errors = array())
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$category = $this->getCategory($project['id']);
- $this->response->html($this->projectLayout('category_edit', array(
- 'values' => $category,
- 'errors' => array(),
+ $this->response->html($this->projectLayout('category/edit', array(
+ 'values' => empty($values) ? $category : $values,
+ 'errors' => $errors,
'project' => $project,
- 'menu' => 'projects',
'title' => t('Categories')
)));
}
@@ -107,7 +98,7 @@ class Category extends Base
*/
public function update()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$values = $this->request->getValues();
list($valid, $errors) = $this->category->validateModification($values);
@@ -123,13 +114,7 @@ class Category extends Base
}
}
- $this->response->html($this->projectLayout('category_edit', array(
- 'values' => $values,
- 'errors' => $errors,
- 'project' => $project,
- 'menu' => 'projects',
- 'title' => t('Categories')
- )));
+ $this->edit($values, $errors);
}
/**
@@ -139,13 +124,12 @@ class Category extends Base
*/
public function confirm()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$category = $this->getCategory($project['id']);
- $this->response->html($this->projectLayout('category_remove', array(
+ $this->response->html($this->projectLayout('category/remove', array(
'project' => $project,
'category' => $category,
- 'menu' => 'projects',
'title' => t('Remove a category')
)));
}
@@ -158,7 +142,7 @@ class Category extends Base
public function remove()
{
$this->checkCSRFParam();
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$category = $this->getCategory($project['id']);
if ($this->category->remove($category['id'])) {
diff --git a/app/Controller/Column.php b/app/Controller/Column.php
new file mode 100644
index 00000000..89c495a6
--- /dev/null
+++ b/app/Controller/Column.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Column controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Column extends Base
+{
+ /**
+ * Display columns list
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+ $columns = $this->board->getColumns($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']),
+ 'columns' => $columns,
+ 'project' => $project,
+ 'title' => t('Edit board')
+ )));
+ }
+
+ /**
+ * Validate and add a new column
+ *
+ * @access public
+ */
+ public function create()
+ {
+ $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;
+ }
+
+ list($valid, $errors) = $this->board->validateCreation($data);
+
+ 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'])));
+ }
+ else {
+ $this->session->flashError(t('Unable to update this board.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Display a form to edit a column
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+ $column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
+
+ $this->response->html($this->projectLayout('column/edit', array(
+ 'errors' => $errors,
+ 'values' => $values ?: $column,
+ 'project' => $project,
+ 'column' => $column,
+ 'title' => t('Edit column "%s"', $column['title'])
+ )));
+ }
+
+ /**
+ * Validate and update a column
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->board->validateModification($values);
+
+ if ($valid) {
+
+ if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) {
+ $this->session->flash(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->edit($values, $errors);
+ }
+
+ /**
+ * Move a column up or down
+ *
+ * @access public
+ */
+ public function move()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $column_id = $this->request->getIntegerParam('column_id');
+ $direction = $this->request->getStringParam('direction');
+
+ if ($direction === 'up' || $direction === 'down') {
+ $this->board->{'move'.$direction}($project['id'], $column_id);
+ }
+
+ $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])));
+ }
+
+ /**
+ * Confirm column suppression
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->projectLayout('column/remove', array(
+ 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')),
+ 'project' => $project,
+ 'title' => t('Remove a column from a board')
+ )));
+ }
+
+ /**
+ * Remove a column
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $project = $this->getProject();
+ $this->checkCSRFParam();
+ $column = $this->board->getColumn($this->request->getIntegerParam('column_id'));
+
+ if (! empty($column) && $this->board->removeColumn($column['id'])) {
+ $this->session->flash(t('Column removed successfully.'));
+ }
+ else {
+ $this->session->flashError(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 a9032ed8..a5f6b1f8 100644
--- a/app/Controller/Comment.php
+++ b/app/Controller/Comment.php
@@ -20,13 +20,12 @@ class Comment extends Base
{
$comment = $this->comment->getById($this->request->getIntegerParam('comment_id'));
- if (! $comment) {
+ if (empty($comment)) {
$this->notfound();
}
- if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) {
- $this->response->html($this->template->layout('comment_forbidden', array(
- 'menu' => 'tasks',
+ if (! $this->userSession->isAdmin() && $comment['user_id'] != $this->userSession->getId()) {
+ $this->response->html($this->template->layout('comment/forbidden', array(
'title' => t('Access Forbidden')
)));
}
@@ -39,19 +38,32 @@ class Comment extends Base
*
* @access public
*/
- public function create()
+ public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
+ $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
- $this->response->html($this->taskLayout('comment_create', array(
- 'values' => array(
- 'user_id' => $this->acl->getUserId(),
+ if (empty($values)) {
+ $values = array(
+ 'user_id' => $this->userSession->getId(),
'task_id' => $task['id'],
- ),
- 'errors' => array(),
+ );
+ }
+
+ 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(
+ 'values' => $values,
+ 'errors' => $errors,
'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Add a comment')
+ 'title' => t('Add a comment'),
)));
}
@@ -64,6 +76,7 @@ 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);
@@ -76,16 +89,14 @@ class Comment extends Base
$this->session->flashError(t('Unable to create your comment.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments');
+ if ($ajax) {
+ $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
+ }
+
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments');
}
- $this->response->html($this->taskLayout('comment_create', array(
- 'values' => $values,
- 'errors' => $errors,
- 'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Add a comment')
- )));
+ $this->create($values, $errors);
}
/**
@@ -93,17 +104,16 @@ class Comment extends Base
*
* @access public
*/
- public function edit()
+ public function edit(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$comment = $this->getComment();
- $this->response->html($this->taskLayout('comment_edit', array(
- 'values' => $comment,
- 'errors' => array(),
+ $this->response->html($this->taskLayout('comment/edit', array(
+ 'values' => empty($values) ? $comment : $values,
+ 'errors' => $errors,
'comment' => $comment,
'task' => $task,
- 'menu' => 'tasks',
'title' => t('Edit a comment')
)));
}
@@ -130,17 +140,10 @@ class Comment extends Base
$this->session->flashError(t('Unable to update your comment.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comment-'.$comment['id']);
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comment-'.$comment['id']);
}
- $this->response->html($this->taskLayout('comment_edit', array(
- 'values' => $values,
- 'errors' => $errors,
- 'comment' => $comment,
- 'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Edit a comment')
- )));
+ $this->edit($values, $errors);
}
/**
@@ -153,10 +156,9 @@ class Comment extends Base
$task = $this->getTask();
$comment = $this->getComment();
- $this->response->html($this->taskLayout('comment_remove', array(
+ $this->response->html($this->taskLayout('comment/remove', array(
'comment' => $comment,
'task' => $task,
- 'menu' => 'tasks',
'title' => t('Remove a comment')
)));
}
@@ -179,6 +181,6 @@ class Comment extends Base
$this->session->flashError(t('Unable to remove this comment.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments');
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments');
}
}
diff --git a/app/Controller/Config.php b/app/Controller/Config.php
index 7c8373c3..fbd374ab 100644
--- a/app/Controller/Config.php
+++ b/app/Controller/Config.php
@@ -20,12 +20,12 @@ class Config extends Base
*/
private function layout($template, array $params)
{
+ $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
$params['values'] = $this->config->getAll();
$params['errors'] = array();
- $params['menu'] = 'config';
- $params['config_content_for_layout'] = $this->template->load($template, $params);
+ $params['config_content_for_layout'] = $this->template->render($template, $params);
- return $this->template->layout('config_layout', $params);
+ return $this->template->layout('config/layout', $params);
}
/**
@@ -38,7 +38,19 @@ class Config extends Base
{
if ($this->request->isPost()) {
- $values = $this->request->getValues();
+ $values = $this->request->getValues();
+
+ switch ($redirect) {
+ case 'project':
+ $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0);
+ break;
+ case 'integrations':
+ $values += array('integration_slack_webhook' => 0, 'integration_hipchat' => 0, 'integration_gravatar' => 0, 'integration_jabber' => 0);
+ break;
+ case 'calendar':
+ $values += array('calendar_user_subtasks_forecast' => 0, 'calendar_user_subtasks_time_tracking' => 0);
+ break;
+ }
if ($this->config->save($values)) {
$this->config->reload();
@@ -59,9 +71,9 @@ class Config extends Base
*/
public function index()
{
- $this->response->html($this->layout('config_about', array(
+ $this->response->html($this->layout('config/about', array(
'db_size' => $this->config->getDatabaseSize(),
- 'title' => t('About'),
+ 'title' => t('Settings').' &gt; '.t('About'),
)));
}
@@ -74,11 +86,26 @@ class Config extends Base
{
$this->common('application');
- $this->response->html($this->layout('config_application', array(
- 'title' => t('Application settings'),
+ $this->response->html($this->layout('config/application', array(
'languages' => $this->config->getLanguages(),
'timezones' => $this->config->getTimezones(),
'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'title' => t('Settings').' &gt; '.t('Application settings'),
+ )));
+ }
+
+ /**
+ * Display the project settings page
+ *
+ * @access public
+ */
+ public function project()
+ {
+ $this->common('project');
+
+ $this->response->html($this->layout('config/project', array(
+ 'default_columns' => implode(', ', $this->board->getDefaultColumns()),
+ 'title' => t('Settings').' &gt; '.t('Project settings'),
)));
}
@@ -91,9 +118,36 @@ class Config extends Base
{
$this->common('board');
- $this->response->html($this->layout('config_board', array(
- 'title' => t('Board settings'),
- 'default_columns' => implode(', ', $this->board->getDefaultColumns()),
+ $this->response->html($this->layout('config/board', array(
+ 'title' => t('Settings').' &gt; '.t('Board settings'),
+ )));
+ }
+
+ /**
+ * Display the calendar settings page
+ *
+ * @access public
+ */
+ public function calendar()
+ {
+ $this->common('calendar');
+
+ $this->response->html($this->layout('config/calendar', array(
+ 'title' => t('Settings').' &gt; '.t('Calendar settings'),
+ )));
+ }
+
+ /**
+ * Display the integration settings page
+ *
+ * @access public
+ */
+ public function integrations()
+ {
+ $this->common('integrations');
+
+ $this->response->html($this->layout('config/integrations', array(
+ 'title' => t('Settings').' &gt; '.t('Integrations'),
)));
}
@@ -106,8 +160,8 @@ class Config extends Base
{
$this->common('webhook');
- $this->response->html($this->layout('config_webhook', array(
- 'title' => t('Webhook settings'),
+ $this->response->html($this->layout('config/webhook', array(
+ 'title' => t('Settings').' &gt; '.t('Webhook settings'),
)));
}
@@ -118,8 +172,8 @@ class Config extends Base
*/
public function api()
{
- $this->response->html($this->layout('config_api', array(
- 'title' => t('API'),
+ $this->response->html($this->layout('config/api', array(
+ 'title' => t('Settings').' &gt; '.t('API'),
)));
}
diff --git a/app/Controller/Currency.php b/app/Controller/Currency.php
new file mode 100644
index 00000000..10fb90da
--- /dev/null
+++ b/app/Controller/Currency.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Currency controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+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(
+ 'config_values' => array('application_currency' => $this->config->get('application_currency')),
+ 'values' => $values,
+ 'errors' => $errors,
+ 'rates' => $this->currency->getAll(),
+ 'currencies' => $this->config->getCurrencies(),
+ 'title' => t('Settings').' &gt; '.t('Currency rates'),
+ )));
+ }
+
+ /**
+ * Validate and save a new currency rate
+ *
+ * @access public
+ */
+ public function create()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->currency->validate($values);
+
+ if ($valid) {
+
+ if ($this->currency->create($values['currency'], $values['rate'])) {
+ $this->session->flash(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->index($values, $errors);
+ }
+
+ /**
+ * Save reference currency
+ *
+ * @access public
+ */
+ public function reference()
+ {
+ $values = $this->request->getValues();
+
+ if ($this->config->save($values)) {
+ $this->config->reload();
+ $this->session->flash(t('Settings saved successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to save your settings.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('currency', 'index'));
+ }
+}
diff --git a/app/Controller/Export.php b/app/Controller/Export.php
new file mode 100644
index 00000000..117fb5ee
--- /dev/null
+++ b/app/Controller/Export.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Export controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Export extends Base
+{
+ /**
+ * Common export method
+ *
+ * @access private
+ */
+ private function common($model, $method, $filename, $action, $page_title)
+ {
+ $project = $this->getProject();
+ $from = $this->request->getStringParam('from');
+ $to = $this->request->getStringParam('to');
+
+ if ($from && $to) {
+ $data = $this->$model->$method($project['id'], $from, $to);
+ $this->response->forceDownload($filename.'.csv');
+ $this->response->csv($data);
+ }
+
+ $this->response->html($this->projectLayout('export/'.$action, array(
+ 'values' => array(
+ 'controller' => 'export',
+ 'action' => $action,
+ 'project_id' => $project['id'],
+ 'from' => $from,
+ 'to' => $to,
+ ),
+ 'errors' => array(),
+ 'date_format' => $this->config->get('application_date_format'),
+ 'date_formats' => $this->dateParser->getAvailableFormats(),
+ 'project' => $project,
+ 'title' => $page_title,
+ ), 'export/sidebar'));
+ }
+
+ /**
+ * Task export
+ *
+ * @access public
+ */
+ public function tasks()
+ {
+ $this->common('taskExport', 'export', t('Tasks'), 'tasks', t('Tasks Export'));
+ }
+
+ /**
+ * Subtask export
+ *
+ * @access public
+ */
+ public function subtasks()
+ {
+ $this->common('subtaskExport', 'export', t('Subtasks'), 'subtasks', t('Subtasks Export'));
+ }
+
+ /**
+ * Daily project summary export
+ *
+ * @access public
+ */
+ public function summary()
+ {
+ $this->common('projectDailySummary', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export'));
+ }
+
+ /**
+ * Transition export
+ *
+ * @access public
+ */
+ public function transitions()
+ {
+ $this->common('transition', 'export', t('Transitions'), 'transitions', t('Task transitions export'));
+ }
+}
diff --git a/app/Controller/File.php b/app/Controller/File.php
index 3c8c32d1..f0367537 100644
--- a/app/Controller/File.php
+++ b/app/Controller/File.php
@@ -2,8 +2,6 @@
namespace Controller;
-use Model\File as FileModel;
-
/**
* File controller
*
@@ -13,6 +11,32 @@ use Model\File as FileModel;
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'))) {
+
+ $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
@@ -21,11 +45,9 @@ class File extends Base
{
$task = $this->getTask();
- $this->response->html($this->taskLayout('file_new', array(
+ $this->response->html($this->taskLayout('file/new', array(
'task' => $task,
- 'menu' => 'tasks',
'max_size' => ini_get('upload_max_filesize'),
- 'title' => t('Attach a document')
)));
}
@@ -38,13 +60,11 @@ class File extends Base
{
$task = $this->getTask();
- if ($this->file->upload($task['project_id'], $task['id'], 'files') === true) {
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#attachments');
- }
- else {
+ if (! $this->file->upload($task['project_id'], $task['id'], 'files')) {
$this->session->flashError(t('Unable to upload the file.'));
- $this->response->redirect('?controller=file&action=create&task_id='.$task['id']);
}
+
+ $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
/**
@@ -56,14 +76,14 @@ class File extends Base
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
- $filename = FileModel::BASE_PATH.$file['path'];
+ $filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$this->response->forceDownload($file['name']);
$this->response->binary(file_get_contents($filename));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
+ $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
/**
@@ -77,8 +97,9 @@ class File extends Base
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
if ($file['task_id'] == $task['id']) {
- $this->response->html($this->template->load('file_open', array(
- 'file' => $file
+ $this->response->html($this->template->render('file/open', array(
+ 'file' => $file,
+ 'task' => $task,
)));
}
}
@@ -92,7 +113,7 @@ class File extends Base
{
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
- $filename = FileModel::BASE_PATH.$file['path'];
+ $filename = FILES_DIR.$file['path'];
if ($file['task_id'] == $task['id'] && file_exists($filename)) {
$metadata = getimagesize($filename);
@@ -105,6 +126,28 @@ class File extends Base
}
/**
+ * Return image thumbnails
+ *
+ * @access public
+ */
+ public function thumbnail()
+ {
+ $task = $this->getTask();
+ $file = $this->file->getById($this->request->getIntegerParam('file_id'));
+ $filename = FILES_DIR.$file['path'];
+
+ if ($file['task_id'] == $task['id'] && file_exists($filename)) {
+
+ $this->response->contentType('image/jpeg');
+ $this->file->generateThumbnail(
+ $filename,
+ $this->request->getIntegerParam('width'),
+ $this->request->getIntegerParam('height')
+ );
+ }
+ }
+
+ /**
* Remove a file
*
* @access public
@@ -121,7 +164,7 @@ class File extends Base
$this->session->flashError(t('Unable to remove this file.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
+ $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
}
/**
@@ -134,11 +177,9 @@ class File extends Base
$task = $this->getTask();
$file = $this->file->getById($this->request->getIntegerParam('file_id'));
- $this->response->html($this->taskLayout('file_remove', array(
+ $this->response->html($this->taskLayout('file/remove', array(
'task' => $task,
'file' => $file,
- 'menu' => 'tasks',
- 'title' => t('Remove a file')
)));
}
}
diff --git a/app/Controller/Hourlyrate.php b/app/Controller/Hourlyrate.php
new file mode 100644
index 00000000..19650ede
--- /dev/null
+++ b/app/Controller/Hourlyrate.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Hourly Rate controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Hourlyrate extends User
+{
+ /**
+ * Display rate and form
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('hourlyrate/index', array(
+ 'rates' => $this->hourlyRate->getAllByUser($user['id']),
+ 'currencies_list' => $this->config->getCurrencies(),
+ 'values' => $values + array('user_id' => $user['id']),
+ 'errors' => $errors,
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Validate and save a new rate
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->hourlyRate->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->hourlyRate->create($values['user_id'], $values['rate'], $values['currency'], $values['date_effective'])) {
+ $this->session->flash(t('Hourly rate created successfully.'));
+ $this->response->redirect($this->helper->url->to('hourlyrate', 'index', array('user_id' => $values['user_id'])));
+ }
+ else {
+ $this->session->flashError(t('Unable to save the hourly rate.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Confirmation dialag box to remove a row
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('hourlyrate/remove', array(
+ 'rate_id' => $this->request->getIntegerParam('rate_id'),
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Remove a row
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $user = $this->getUser();
+
+ if ($this->hourlyRate->remove($this->request->getIntegerParam('rate_id'))) {
+ $this->session->flash(t('Rate removed successfully.'));
+ }
+ else {
+ $this->session->flash(t('Unable to remove this rate.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('hourlyrate', 'index', array('user_id' => $user['id'])));
+ }
+}
diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php
new file mode 100644
index 00000000..52e10fa1
--- /dev/null
+++ b/app/Controller/Ical.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Controller;
+
+use Model\TaskFilter;
+use Model\Task as TaskModel;
+use Eluceo\iCal\Component\Calendar as iCalendar;
+
+/**
+ * iCalendar controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Ical extends Base
+{
+ /**
+ * Get user iCalendar
+ *
+ * @access public
+ */
+ public function user()
+ {
+ $token = $this->request->getStringParam('token');
+ $user = $this->user->getByToken($token);
+
+ // Token verification
+ if (empty($user)) {
+ $this->forbidden(true);
+ }
+
+ // Common filter
+ $filter = $this->taskFilter
+ ->create()
+ ->filterByOwner($user['id']);
+
+ // Calendar properties
+ $calendar = new iCalendar('Kanboard');
+ $calendar->setName($user['name'] ?: $user['username']);
+ $calendar->setDescription($user['name'] ?: $user['username']);
+ $calendar->setPublishedTTL('PT1H');
+
+ $this->renderCalendar($filter, $calendar);
+ }
+
+ /**
+ * Get project iCalendar
+ *
+ * @access public
+ */
+ public function project()
+ {
+ $token = $this->request->getStringParam('token');
+ $project = $this->project->getByToken($token);
+
+ // Token verification
+ if (empty($project)) {
+ $this->forbidden(true);
+ }
+
+ // Common filter
+ $filter = $this->taskFilter
+ ->create()
+ ->filterByProject($project['id']);
+
+ // Calendar properties
+ $calendar = new iCalendar('Kanboard');
+ $calendar->setName($project['name']);
+ $calendar->setDescription($project['name']);
+ $calendar->setPublishedTTL('PT1H');
+
+ $this->renderCalendar($filter, $calendar);
+ }
+
+ /**
+ * Common method to render iCal events
+ *
+ * @access private
+ */
+ private function renderCalendar(TaskFilter $filter, iCalendar $calendar)
+ {
+ $start = $this->request->getStringParam('start', strtotime('-1 month'));
+ $end = $this->request->getStringParam('end', strtotime('+2 months'));
+
+ // Tasks
+ if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
+ $filter->copy()->filterByCreationDateRange($start, $end)->addDateTimeIcalEvents('date_creation', 'date_completed', $calendar);
+ }
+ else {
+ $filter->copy()->filterByStartDateRange($start, $end)->addDateTimeIcalEvents('date_started', 'date_completed', $calendar);
+ }
+
+ // Tasks with due date
+ $filter->copy()->filterByDueDateRange($start, $end)->addAllDayIcalEvents('date_due', $calendar);
+
+ $this->response->contentType('text/calendar; charset=utf-8');
+ echo $calendar->render();
+ }
+}
diff --git a/app/Controller/Link.php b/app/Controller/Link.php
new file mode 100644
index 00000000..482e415c
--- /dev/null
+++ b/app/Controller/Link.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Link controller
+ *
+ * @package controller
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+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
+ * @return array
+ */
+ private function getLink()
+ {
+ $link = $this->link->getById($this->request->getIntegerParam('link_id'));
+
+ if (empty($link)) {
+ $this->notfound();
+ }
+
+ return $link;
+ }
+
+ /**
+ * List of links
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $this->response->html($this->layout('link/index', array(
+ 'links' => $this->link->getMergedList(),
+ 'values' => $values,
+ 'errors' => $errors,
+ 'title' => t('Settings').' &gt; '.t('Task\'s links'),
+ )));
+ }
+
+ /**
+ * Validate and save a new link
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->link->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->link->create($values['label'], $values['opposite_label']) !== false) {
+ $this->session->flash(t('Link added successfully.'));
+ $this->response->redirect($this->helper->url->to('link', 'index'));
+ }
+ else {
+ $this->session->flashError(t('Unable to create your link.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Edit form
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $link = $this->getLink();
+ $link['label'] = t($link['label']);
+
+ $this->response->html($this->layout('link/edit', array(
+ 'values' => $values ?: $link,
+ 'errors' => $errors,
+ 'labels' => $this->link->getList($link['id']),
+ 'link' => $link,
+ 'title' => t('Link modification')
+ )));
+ }
+
+ /**
+ * Edit a link (validate the form and update the database)
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->link->validateModification($values);
+
+ if ($valid) {
+ if ($this->link->update($values)) {
+ $this->session->flash(t('Link updated successfully.'));
+ $this->response->redirect($this->helper->url->to('link', 'index'));
+ }
+ else {
+ $this->session->flashError(t('Unable to update your link.'));
+ }
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a link
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $link = $this->getLink();
+
+ $this->response->html($this->layout('link/remove', array(
+ 'link' => $link,
+ 'title' => t('Remove a link')
+ )));
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $link = $this->getLink();
+
+ if ($this->link->remove($link['id'])) {
+ $this->session->flash(t('Link removed successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to remove this link.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('link', 'index'));
+ }
+}
diff --git a/app/Controller/Project.php b/app/Controller/Project.php
index d749ef53..ba039b7d 100644
--- a/app/Controller/Project.php
+++ b/app/Controller/Project.php
@@ -2,10 +2,8 @@
namespace Controller;
-use Model\Task as TaskModel;
-
/**
- * Project controller
+ * Project controller (Settings + creation/edition)
*
* @package controller
* @author Frederic Guillot
@@ -19,25 +17,26 @@ class Project extends Base
*/
public function index()
{
- $projects = $this->project->getAll($this->acl->isRegularUser());
- $nb_projects = count($projects);
- $active_projects = array();
- $inactive_projects = array();
-
- foreach ($projects as $project) {
- if ($project['is_active'] == 1) {
- $active_projects[] = $project;
- }
- else {
- $inactive_projects[] = $project;
- }
+ if ($this->userSession->isAdmin()) {
+ $project_ids = $this->project->getAllIds();
}
+ else {
+ $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId());
+ }
+
+ $nb_projects = count($project_ids);
- $this->response->html($this->template->layout('project_index', array(
- 'active_projects' => $active_projects,
- 'inactive_projects' => $inactive_projects,
+ $paginator = $this->paginator
+ ->setUrl('project', 'index')
+ ->setMax(20)
+ ->setOrder('name')
+ ->setQuery($this->project->getQueryColumnStats($project_ids))
+ ->calculate();
+
+ $this->response->html($this->template->layout('project/index', array(
+ 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'paginator' => $paginator,
'nb_projects' => $nb_projects,
- 'menu' => 'projects',
'title' => t('Projects').' ('.$nb_projects.')'
)));
}
@@ -51,54 +50,21 @@ class Project extends Base
{
$project = $this->getProject();
- $this->response->html($this->projectLayout('project_show', array(
+ $this->response->html($this->projectLayout('project/show', array(
'project' => $project,
- 'stats' => $this->project->getStats($project['id']),
+ 'stats' => $this->project->getTaskStats($project['id']),
'title' => $project['name'],
)));
}
/**
- * Task export
- *
- * @access public
- */
- public function export()
- {
- $project = $this->getProjectManagement();
- $from = $this->request->getStringParam('from');
- $to = $this->request->getStringParam('to');
-
- if ($from && $to) {
- $data = $this->taskExport->export($project['id'], $from, $to);
- $this->response->forceDownload('Export_'.date('Y_m_d_H_i_S').'.csv');
- $this->response->csv($data);
- }
-
- $this->response->html($this->projectLayout('project_export', array(
- 'values' => array(
- 'controller' => 'project',
- 'action' => 'export',
- 'project_id' => $project['id'],
- 'from' => $from,
- 'to' => $to,
- ),
- 'errors' => array(),
- 'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
- 'project' => $project,
- 'title' => t('Tasks Export')
- )));
- }
-
- /**
* Public access management
*
* @access public
*/
public function share()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$switch = $this->request->getStringParam('switch');
if ($switch === 'enable' || $switch === 'disable') {
@@ -114,24 +80,51 @@ class Project extends Base
$this->response->redirect('?controller=project&action=share&project_id='.$project['id']);
}
- $this->response->html($this->projectLayout('project_share', array(
+ $this->response->html($this->projectLayout('project/share', array(
'project' => $project,
'title' => t('Public access'),
)));
}
/**
- * Display a form to edit a project
+ * Integrations page
*
* @access public
*/
- public function edit()
+ public function integration()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
+
+ if ($this->request->isPost()) {
+ $params = $this->request->getValues();
+ $params += array('hipchat' => 0, 'slack' => 0, 'jabber' => 0);
+ $this->projectIntegration->saveParameters($project['id'], $params);
+ }
- $this->response->html($this->projectLayout('project_edit', array(
+ $values = $this->projectIntegration->getParameters($project['id']);
+ $values += array('hipchat_api_url' => 'https://api.hipchat.com');
+
+ $this->response->html($this->projectLayout('project/integrations', array(
+ 'project' => $project,
+ 'title' => t('Integrations'),
+ 'webhook_token' => $this->config->get('webhook_token'),
+ 'values' => $values,
'errors' => array(),
- 'values' => $project,
+ )));
+ }
+
+ /**
+ * 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')
)));
@@ -144,8 +137,13 @@ class Project extends Base
*/
public function update()
{
- $project = $this->getProjectManagement();
- $values = $this->request->getValues() + array('is_active' => 0);
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ if ($project['is_private'] == 1 && $this->userSession->isAdmin() && ! isset($values['is_private'])) {
+ $values += array('is_private' => 0);
+ }
+
list($valid, $errors) = $this->project->validateModification($values);
if ($valid) {
@@ -159,12 +157,7 @@ class Project extends Base
}
}
- $this->response->html($this->projectLayout('project_edit', array(
- 'errors' => $errors,
- 'values' => $values,
- 'project' => $project,
- 'title' => t('Edit Project')
- )));
+ $this->edit($values, $errors);
}
/**
@@ -174,9 +167,9 @@ class Project extends Base
*/
public function users()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
- $this->response->html($this->projectLayout('project_users', array(
+ $this->response->html($this->projectLayout('project/users', array(
'project' => $project,
'users' => $this->projectPermission->getAllUsers($project['id']),
'title' => t('Edit project access list')
@@ -190,7 +183,7 @@ class Project extends Base
*/
public function allowEverybody()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
$values = $this->request->getValues() + array('is_everybody_allowed' => 0);
list($valid,) = $this->projectPermission->validateProjectModification($values);
@@ -219,7 +212,37 @@ class Project extends Base
if ($valid) {
- if ($this->projectPermission->allowUser($values['project_id'], $values['user_id'])) {
+ 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('?controller=project&action=users&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 {
@@ -248,7 +271,7 @@ class Project extends Base
if ($valid) {
- if ($this->projectPermission->revokeUser($values['project_id'], $values['user_id'])) {
+ if ($this->projectPermission->revokeMember($values['project_id'], $values['user_id'])) {
$this->session->flash(t('Project updated successfully.'));
}
else {
@@ -266,7 +289,7 @@ class Project extends Base
*/
public function remove()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
if ($this->request->getStringParam('remove') === 'yes') {
@@ -281,7 +304,7 @@ class Project extends Base
$this->response->redirect('?controller=project');
}
- $this->response->html($this->projectLayout('project_remove', array(
+ $this->response->html($this->projectLayout('project/remove', array(
'project' => $project,
'title' => t('Remove project')
)));
@@ -291,17 +314,16 @@ class Project extends Base
* Duplicate a project
*
* @author Antonio Rabelo
+ * @author Michael Lüpkes
* @access public
*/
public function duplicate()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
if ($this->request->getStringParam('duplicate') === 'yes') {
-
- $this->checkCSRFParam();
-
- if ($this->project->duplicate($project['id'])) {
+ $values = array_keys($this->request->getValues());
+ if ($this->projectDuplication->duplicate($project['id'], $values) !== false) {
$this->session->flash(t('Project cloned successfully.'));
} else {
$this->session->flashError(t('Unable to clone this project.'));
@@ -310,7 +332,7 @@ class Project extends Base
$this->response->redirect('?controller=project');
}
- $this->response->html($this->projectLayout('project_duplicate', array(
+ $this->response->html($this->projectLayout('project/duplicate', array(
'project' => $project,
'title' => t('Clone this project')
)));
@@ -323,7 +345,7 @@ class Project extends Base
*/
public function disable()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
if ($this->request->getStringParam('disable') === 'yes') {
@@ -338,7 +360,7 @@ class Project extends Base
$this->response->redirect('?controller=project&action=show&project_id='.$project['id']);
}
- $this->response->html($this->projectLayout('project_disable', array(
+ $this->response->html($this->projectLayout('project/disable', array(
'project' => $project,
'title' => t('Project activation')
)));
@@ -351,7 +373,7 @@ class Project extends Base
*/
public function enable()
{
- $project = $this->getProjectManagement();
+ $project = $this->getProject();
if ($this->request->getStringParam('enable') === 'yes') {
@@ -366,7 +388,7 @@ class Project extends Base
$this->response->redirect('?controller=project&action=show&project_id='.$project['id']);
}
- $this->response->html($this->projectLayout('project_enable', array(
+ $this->response->html($this->projectLayout('project/enable', array(
'project' => $project,
'title' => t('Project activation')
)));
@@ -383,115 +405,13 @@ class Project extends Base
$project = $this->project->getByToken($token);
// Token verification
- if (! $project) {
+ if (empty($project)) {
$this->forbidden(true);
}
- $this->response->xml($this->template->load('project_feed', array(
- 'events' => $this->projectActivity->getProject($project['id']),
- 'project' => $project,
- )));
- }
-
- /**
- * Activity page for a project
- *
- * @access public
- */
- public function activity()
- {
- $project = $this->getProject();
-
- $this->response->html($this->template->layout('project_activity', array(
+ $this->response->xml($this->template->render('project/feed', array(
'events' => $this->projectActivity->getProject($project['id']),
- 'menu' => 'projects',
'project' => $project,
- 'title' => t('%s\'s activity', $project['name'])
- )));
- }
-
- /**
- * Task search for a given project
- *
- * @access public
- */
- public function search()
- {
- $project = $this->getProject();
- $search = $this->request->getStringParam('search');
- $direction = $this->request->getStringParam('direction', 'DESC');
- $order = $this->request->getStringParam('order', 'tasks.id');
- $offset = $this->request->getIntegerParam('offset', 0);
- $tasks = array();
- $nb_tasks = 0;
- $limit = 25;
-
- if ($search !== '') {
- $tasks = $this->taskFinder->search($project['id'], $search, $offset, $limit, $order, $direction);
- $nb_tasks = $this->taskFinder->countSearch($project['id'], $search);
- }
-
- $this->response->html($this->template->layout('project_search', array(
- 'tasks' => $tasks,
- 'nb_tasks' => $nb_tasks,
- 'pagination' => array(
- 'controller' => 'project',
- 'action' => 'search',
- 'params' => array('search' => $search, 'project_id' => $project['id']),
- 'direction' => $direction,
- 'order' => $order,
- 'total' => $nb_tasks,
- 'offset' => $offset,
- 'limit' => $limit,
- ),
- 'values' => array(
- 'search' => $search,
- 'controller' => 'project',
- 'action' => 'search',
- 'project_id' => $project['id'],
- ),
- 'project' => $project,
- 'menu' => 'projects',
- 'columns' => $this->board->getColumnsList($project['id']),
- 'categories' => $this->category->getList($project['id'], false),
- 'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
- )));
- }
-
- /**
- * List of completed tasks for a given project
- *
- * @access public
- */
- public function tasks()
- {
- $project = $this->getProject();
- $direction = $this->request->getStringParam('direction', 'DESC');
- $order = $this->request->getStringParam('order', 'tasks.date_completed');
- $offset = $this->request->getIntegerParam('offset', 0);
- $limit = 25;
-
- $tasks = $this->taskFinder->getClosedTasks($project['id'], $offset, $limit, $order, $direction);
- $nb_tasks = $this->taskFinder->countByProjectId($project['id'], array(TaskModel::STATUS_CLOSED));
-
- $this->response->html($this->template->layout('project_tasks', array(
- 'pagination' => array(
- 'controller' => 'project',
- 'action' => 'tasks',
- 'params' => array('project_id' => $project['id']),
- 'direction' => $direction,
- 'order' => $order,
- 'total' => $nb_tasks,
- 'offset' => $offset,
- 'limit' => $limit,
- ),
- 'project' => $project,
- 'menu' => 'projects',
- 'columns' => $this->board->getColumnsList($project['id']),
- 'categories' => $this->category->getList($project['id'], false),
- 'tasks' => $tasks,
- 'nb_tasks' => $nb_tasks,
- 'title' => $project['name'].' ('.$nb_tasks.')'
)));
}
@@ -500,14 +420,16 @@ class Project extends Base
*
* @access public
*/
- public function create()
+ public function create(array $values = array(), array $errors = array())
{
- $this->response->html($this->template->layout('project_new', array(
- 'errors' => array(),
- 'values' => array(
- 'is_private' => $this->request->getIntegerParam('private', $this->acl->isRegularUser()),
- ),
- 'title' => t('New project')
+ $is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() ? 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'),
)));
}
@@ -523,19 +445,16 @@ class Project extends Base
if ($valid) {
- if ($this->project->create($values, $this->acl->getUserId())) {
+ $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('?controller=project');
- }
- else {
- $this->session->flashError(t('Unable to create your project.'));
+ $this->response->redirect('?controller=project&action=show&project_id='.$project_id);
}
+
+ $this->session->flashError(t('Unable to create your project.'));
}
- $this->response->html($this->template->layout('project_new', array(
- 'errors' => $errors,
- 'values' => $values,
- 'title' => t('New Project')
- )));
+ $this->create($values, $errors);
}
}
diff --git a/app/Controller/Projectinfo.php b/app/Controller/Projectinfo.php
new file mode 100644
index 00000000..a9498f43
--- /dev/null
+++ b/app/Controller/Projectinfo.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Project Info controller (ActivityStream + completed tasks)
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Projectinfo extends Base
+{
+ /**
+ * Activity page for a project
+ *
+ * @access public
+ */
+ public function activity()
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->template->layout('projectinfo/activity', array(
+ 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'events' => $this->projectActivity->getProject($project['id']),
+ 'project' => $project,
+ 'title' => t('%s\'s activity', $project['name'])
+ )));
+ }
+
+ /**
+ * Task search for a given project
+ *
+ * @access public
+ */
+ public function search()
+ {
+ $project = $this->getProject();
+ $search = $this->request->getStringParam('search');
+ $nb_tasks = 0;
+
+ $paginator = $this->paginator
+ ->setUrl('projectinfo', 'search', array('search' => $search, 'project_id' => $project['id']))
+ ->setMax(30)
+ ->setOrder('tasks.id')
+ ->setDirection('DESC');
+
+ if ($search !== '') {
+
+ $paginator
+ ->setQuery($this->taskFinder->getSearchQuery($project['id'], $search))
+ ->calculate();
+
+ $nb_tasks = $paginator->getTotal();
+ }
+
+ $this->response->html($this->template->layout('projectinfo/search', array(
+ 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'values' => array(
+ 'search' => $search,
+ 'controller' => 'projectinfo',
+ 'action' => 'search',
+ 'project_id' => $project['id'],
+ ),
+ 'paginator' => $paginator,
+ 'project' => $project,
+ 'columns' => $this->board->getColumnsList($project['id']),
+ 'categories' => $this->category->getList($project['id'], false),
+ 'title' => t('Search in the project "%s"', $project['name']).($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
+ )));
+ }
+
+ /**
+ * List of completed tasks for a given project
+ *
+ * @access public
+ */
+ public function tasks()
+ {
+ $project = $this->getProject();
+ $paginator = $this->paginator
+ ->setUrl('projectinfo', 'tasks', array('project_id' => $project['id']))
+ ->setMax(30)
+ ->setOrder('tasks.id')
+ ->setDirection('DESC')
+ ->setQuery($this->taskFinder->getClosedTaskQuery($project['id']))
+ ->calculate();
+
+ $this->response->html($this->template->layout('projectinfo/tasks', array(
+ 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
+ 'project' => $project,
+ 'columns' => $this->board->getColumnsList($project['id']),
+ 'categories' => $this->category->getList($project['id'], false),
+ 'paginator' => $paginator,
+ 'title' => t('Completed tasks for "%s"', $project['name']).' ('.$paginator->getTotal().')'
+ )));
+ }
+}
diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php
index 48f0d6e2..5baa6004 100644
--- a/app/Controller/Subtask.php
+++ b/app/Controller/Subtask.php
@@ -2,8 +2,10 @@
namespace Controller;
+use Model\Subtask as SubtaskModel;
+
/**
- * SubTask controller
+ * Subtask controller
*
* @package controller
* @author Frederic Guillot
@@ -18,9 +20,9 @@ class Subtask extends Base
*/
private function getSubtask()
{
- $subtask = $this->subTask->getById($this->request->getIntegerParam('subtask_id'));
+ $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id'));
- if (! $subtask) {
+ if (empty($subtask)) {
$this->notfound();
}
@@ -32,20 +34,22 @@ class Subtask extends Base
*
* @access public
*/
- public function create()
+ public function create(array $values = array(), array $errors = array())
{
$task = $this->getTask();
- $this->response->html($this->taskLayout('subtask_create', array(
- 'values' => array(
+ if (empty($values)) {
+ $values = array(
'task_id' => $task['id'],
'another_subtask' => $this->request->getIntegerParam('another_subtask', 0)
- ),
- 'errors' => array(),
- 'users_list' => $this->projectPermission->getUsersList($task['project_id']),
+ );
+ }
+
+ $this->response->html($this->taskLayout('subtask/create', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'users_list' => $this->projectPermission->getMemberList($task['project_id']),
'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Add a sub-task')
)));
}
@@ -59,11 +63,11 @@ class Subtask extends Base
$task = $this->getTask();
$values = $this->request->getValues();
- list($valid, $errors) = $this->subTask->validateCreation($values);
+ list($valid, $errors) = $this->subtask->validateCreation($values);
if ($valid) {
- if ($this->subTask->create($values)) {
+ if ($this->subtask->create($values)) {
$this->session->flash(t('Sub-task added successfully.'));
}
else {
@@ -71,20 +75,13 @@ class Subtask extends Base
}
if (isset($values['another_subtask']) && $values['another_subtask'] == 1) {
- $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id'].'&another_subtask=1');
+ $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id'].'&another_subtask=1&project_id='.$task['project_id']);
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks');
}
- $this->response->html($this->taskLayout('subtask_create', array(
- 'values' => $values,
- 'errors' => $errors,
- 'users_list' => $this->projectPermission->getUsersList($task['project_id']),
- 'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Add a sub-task')
- )));
+ $this->create($values, $errors);
}
/**
@@ -92,20 +89,18 @@ class Subtask extends Base
*
* @access public
*/
- public function edit()
+ public function edit(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$subtask = $this->getSubTask();
- $this->response->html($this->taskLayout('subtask_edit', array(
- 'values' => $subtask,
- 'errors' => array(),
- 'users_list' => $this->projectPermission->getUsersList($task['project_id']),
- 'status_list' => $this->subTask->getStatusList(),
+ $this->response->html($this->taskLayout('subtask/edit', array(
+ 'values' => empty($values) ? $subtask : $values,
+ 'errors' => $errors,
+ 'users_list' => $this->projectPermission->getMemberList($task['project_id']),
+ 'status_list' => $this->subtask->getStatusList(),
'subtask' => $subtask,
'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Edit a sub-task')
)));
}
@@ -117,33 +112,24 @@ class Subtask extends Base
public function update()
{
$task = $this->getTask();
- $subtask = $this->getSubtask();
+ $this->getSubtask();
$values = $this->request->getValues();
- list($valid, $errors) = $this->subTask->validateModification($values);
+ list($valid, $errors) = $this->subtask->validateModification($values);
if ($valid) {
- if ($this->subTask->update($values)) {
+ if ($this->subtask->update($values)) {
$this->session->flash(t('Sub-task updated successfully.'));
}
else {
$this->session->flashError(t('Unable to update your sub-task.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks');
}
- $this->response->html($this->taskLayout('subtask_edit', array(
- 'values' => $values,
- 'errors' => $errors,
- 'users_list' => $this->projectPermission->getUsersList($task['project_id']),
- 'status_list' => $this->subTask->getStatusList(),
- 'subtask' => $subtask,
- 'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Edit a sub-task')
- )));
+ $this->edit($values, $errors);
}
/**
@@ -156,11 +142,9 @@ class Subtask extends Base
$task = $this->getTask();
$subtask = $this->getSubtask();
- $this->response->html($this->taskLayout('subtask_remove', array(
+ $this->response->html($this->taskLayout('subtask/remove', array(
'subtask' => $subtask,
'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Remove a sub-task')
)));
}
@@ -175,14 +159,14 @@ class Subtask extends Base
$task = $this->getTask();
$subtask = $this->getSubtask();
- if ($this->subTask->remove($subtask['id'])) {
+ if ($this->subtask->remove($subtask['id'])) {
$this->session->flash(t('Sub-task removed successfully.'));
}
else {
$this->session->flashError(t('Unable to remove this sub-task.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks');
}
/**
@@ -194,17 +178,103 @@ class Subtask extends Base
{
$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/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();
- $value = array(
+ $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' => ($subtask['status'] + 1) % 3,
- 'task_id' => $task['id'],
- );
+ 'status' => SubtaskModel::STATUS_INPROGRESS,
+ ));
- if (! $this->subTask->update($value)) {
- $this->session->flashError(t('Unable to update your sub-task.'));
+ $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'])));
}
+ }
+
+ /**
+ * Move subtask position
+ *
+ * @access public
+ */
+ 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';
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
+ $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');
}
}
diff --git a/app/Controller/Swimlane.php b/app/Controller/Swimlane.php
new file mode 100644
index 00000000..c6862d47
--- /dev/null
+++ b/app/Controller/Swimlane.php
@@ -0,0 +1,256 @@
+<?php
+
+namespace Controller;
+
+use Model\Swimlane as SwimlaneModel;
+
+/**
+ * Swimlanes
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Swimlane extends Base
+{
+ /**
+ * Get the swimlane (common method between actions)
+ *
+ * @access private
+ * @param integer $project_id
+ * @return array
+ */
+ private function getSwimlane($project_id)
+ {
+ $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id'));
+
+ if (empty($swimlane)) {
+ $this->session->flashError(t('Swimlane not found.'));
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project_id);
+ }
+
+ return $swimlane;
+ }
+
+ /**
+ * List of swimlanes for a given project
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->projectLayout('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),
+ 'values' => $values + array('project_id' => $project['id']),
+ 'errors' => $errors,
+ 'project' => $project,
+ 'title' => t('Swimlanes')
+ )));
+ }
+
+ /**
+ * Validate and save a new swimlane
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $project = $this->getProject();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->swimlane->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->swimlane->create($project['id'], $values['name'])) {
+ $this->session->flash(t('Your swimlane have been created successfully.'));
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']);
+ }
+ else {
+ $this->session->flashError(t('Unable to create your swimlane.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Change the default swimlane
+ *
+ * @access public
+ */
+ public function change()
+ {
+ $project = $this->getProject();
+
+ $values = $this->request->getValues() + array('show_default_swimlane' => 0);
+ list($valid,) = $this->swimlane->validateDefaultModification($values);
+
+ if ($valid) {
+
+ if ($this->swimlane->updateDefault($values)) {
+ $this->session->flash(t('The default swimlane have been updated successfully.'));
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']);
+ }
+ else {
+ $this->session->flashError(t('Unable to update this swimlane.'));
+ }
+ }
+
+ $this->index();
+ }
+
+ /**
+ * Edit a swimlane (display the form)
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+ $swimlane = $this->getSwimlane($project['id']);
+
+ $this->response->html($this->projectLayout('swimlane/edit', array(
+ 'values' => empty($values) ? $swimlane : $values,
+ 'errors' => $errors,
+ 'project' => $project,
+ 'title' => t('Swimlanes')
+ )));
+ }
+
+ /**
+ * Edit a swimlane (validate the form and update the database)
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $project = $this->getProject();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->swimlane->validateModification($values);
+
+ if ($valid) {
+
+ if ($this->swimlane->rename($values['id'], $values['name'])) {
+ $this->session->flash(t('Swimlane updated successfully.'));
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']);
+ }
+ else {
+ $this->session->flashError(t('Unable to update this swimlane.'));
+ }
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a swimlane
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $project = $this->getProject();
+ $swimlane = $this->getSwimlane($project['id']);
+
+ $this->response->html($this->projectLayout('swimlane/remove', array(
+ 'project' => $project,
+ 'swimlane' => $swimlane,
+ 'title' => t('Remove a swimlane')
+ )));
+ }
+
+ /**
+ * Remove a swimlane
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $swimlane_id = $this->request->getIntegerParam('swimlane_id');
+
+ if ($this->swimlane->remove($project['id'], $swimlane_id)) {
+ $this->session->flash(t('Swimlane removed successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to remove this swimlane.'));
+ }
+
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']);
+ }
+
+ /**
+ * Disable a swimlane
+ *
+ * @access public
+ */
+ public function disable()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $swimlane_id = $this->request->getIntegerParam('swimlane_id');
+
+ if ($this->swimlane->disable($project['id'], $swimlane_id)) {
+ $this->session->flash(t('Swimlane updated successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to update this swimlane.'));
+ }
+
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']);
+ }
+
+ /**
+ * Enable a swimlane
+ *
+ * @access public
+ */
+ public function enable()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $swimlane_id = $this->request->getIntegerParam('swimlane_id');
+
+ if ($this->swimlane->enable($project['id'], $swimlane_id)) {
+ $this->session->flash(t('Swimlane updated successfully.'));
+ } else {
+ $this->session->flashError(t('Unable to update this swimlane.'));
+ }
+
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']);
+ }
+
+ /**
+ * Move up a swimlane
+ *
+ * @access public
+ */
+ public function moveup()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $swimlane_id = $this->request->getIntegerParam('swimlane_id');
+
+ $this->swimlane->moveUp($project['id'], $swimlane_id);
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']);
+ }
+
+ /**
+ * Move down a swimlane
+ *
+ * @access public
+ */
+ public function movedown()
+ {
+ $this->checkCSRFParam();
+ $project = $this->getProject();
+ $swimlane_id = $this->request->getIntegerParam('swimlane_id');
+
+ $this->swimlane->moveDown($project['id'], $swimlane_id);
+ $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']);
+ }
+}
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index 1b20cf15..dc83f7b1 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -22,20 +22,21 @@ class Task extends Base
$project = $this->project->getByToken($this->request->getStringParam('token'));
// Token verification
- if (! $project) {
+ if (empty($project)) {
$this->forbidden(true);
}
$task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id'));
- if (! $task) {
+ if (empty($task)) {
$this->notfound(true);
}
- $this->response->html($this->template->layout('task_public', array(
+ $this->response->html($this->template->layout('task/public', array(
'project' => $project,
'comments' => $this->comment->getAll($task['id']),
- 'subtasks' => $this->subTask->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']),
'colors_list' => $this->color->getList(),
@@ -54,7 +55,7 @@ class Task extends Base
public function show()
{
$task = $this->getTask();
- $subtasks = $this->subTask->getAll($task['id']);
+ $subtasks = $this->subtask->getAll($task['id']);
$values = array(
'id' => $task['id'],
@@ -65,20 +66,41 @@ class Task extends Base
$this->dateParser->format($values, array('date_started'));
- $this->response->html($this->taskLayout('task_show', array(
+ $this->response->html($this->taskLayout('task/show', array(
'project' => $this->project->getById($task['project_id']),
- 'files' => $this->file->getAll($task['id']),
+ 'files' => $this->file->getAllDocuments($task['id']),
+ 'images' => $this->file->getAllImages($task['id']),
'comments' => $this->comment->getAll($task['id']),
'subtasks' => $subtasks,
+ 'links' => $this->taskLink->getAllGroupedByLabel($task['id']),
'task' => $task,
'values' => $values,
- 'timesheet' => $this->timeTracking->getTaskTimesheet($task, $subtasks),
+ 'link_label_list' => $this->link->getList(0, false),
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->color->getList(),
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
- 'menu' => 'tasks',
+ 'title' => $task['project_name'].' &gt; '.$task['title'],
+ 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
+ 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
+ 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
+ )));
+ }
+
+ /**
+ * Display task activities
+ *
+ * @access public
+ */
+ public function activites()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->taskLayout('task/activity', array(
'title' => $task['title'],
+ 'task' => $task,
+ 'ajax' => $this->request->isAjax(),
+ 'events' => $this->projectActivity->getTask($task['id']),
)));
}
@@ -87,29 +109,36 @@ class Task extends Base
*
* @access public
*/
- public function create()
+ public function create(array $values = array(), array $errors = array())
{
- $project_id = $this->request->getIntegerParam('project_id');
- $this->checkProjectPermissions($project_id);
+ $project = $this->getProject();
+ $method = $this->request->isAjax() ? 'render' : 'layout';
+ $swimlanes_list = $this->swimlane->getList($project['id'], false, true);
- $this->response->html($this->template->layout('task_new', array(
- 'errors' => array(),
- 'values' => array(
- 'project_id' => $project_id,
+ 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'),
'owner_id' => $this->request->getIntegerParam('owner_id'),
'another_task' => $this->request->getIntegerParam('another_task'),
- ),
+ );
+ }
+
+ $this->response->html($this->template->$method('task/new', array(
+ 'ajax' => $this->request->isAjax(),
+ 'errors' => $errors,
+ 'values' => $values + array('project_id' => $project['id']),
'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE),
- 'columns_list' => $this->board->getColumnsList($project_id),
- 'users_list' => $this->projectPermission->getUsersList($project_id),
+ 'columns_list' => $this->board->getColumnsList($project['id']),
+ 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true),
'colors_list' => $this->color->getList(),
- 'categories_list' => $this->category->getList($project_id),
+ 'categories_list' => $this->category->getList($project['id']),
+ 'swimlanes_list' => $swimlanes_list,
'date_format' => $this->config->get('application_date_format'),
'date_formats' => $this->dateParser->getAvailableFormats(),
- 'menu' => 'tasks',
- 'title' => t('New task')
+ 'title' => $project['name'].' &gt; '.t('New task')
)));
}
@@ -120,16 +149,15 @@ class Task extends Base
*/
public function save()
{
+ $project = $this->getProject();
$values = $this->request->getValues();
- $values['creator_id'] = $this->acl->getUserId();
-
- $this->checkProjectPermissions($values['project_id']);
+ $values['creator_id'] = $this->userSession->getId();
list($valid, $errors) = $this->taskValidator->validateCreation($values);
if ($valid) {
- if ($this->task->create($values)) {
+ if ($this->taskCreation->create($values)) {
$this->session->flash(t('Task created successfully.'));
if (isset($values['another_task']) && $values['another_task'] == 1) {
@@ -138,7 +166,7 @@ class Task extends Base
$this->response->redirect('?controller=task&action=create&'.http_build_query($values));
}
else {
- $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']);
+ $this->response->redirect('?controller=board&action=show&project_id='.$project['id']);
}
}
else {
@@ -146,19 +174,7 @@ class Task extends Base
}
}
- $this->response->html($this->template->layout('task_new', array(
- 'errors' => $errors,
- 'values' => $values,
- 'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE),
- 'columns_list' => $this->board->getColumnsList($values['project_id']),
- 'users_list' => $this->projectPermission->getUsersList($values['project_id']),
- 'colors_list' => $this->color->getList(),
- 'categories_list' => $this->category->getList($values['project_id']),
- 'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
- 'menu' => 'tasks',
- 'title' => t('New task')
- )));
+ $this->create($values, $errors);
}
/**
@@ -166,32 +182,34 @@ class Task extends Base
*
* @access public
*/
- public function edit()
+ public function edit(array $values = array(), array $errors = array())
{
$task = $this->getTask();
$ajax = $this->request->isAjax();
- $this->dateParser->format($task, array('date_due'));
+ if (empty($values)) {
+ $values = $task;
+ }
+
+ $this->dateParser->format($values, array('date_due'));
$params = array(
- 'values' => $task,
- 'errors' => array(),
+ 'values' => $values,
+ 'errors' => $errors,
'task' => $task,
- 'users_list' => $this->projectPermission->getUsersList($task['project_id']),
+ 'users_list' => $this->projectPermission->getMemberList($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,
- 'menu' => 'tasks',
- 'title' => t('Edit a task')
);
if ($ajax) {
- $this->response->html($this->template->load('task_edit', $params));
+ $this->response->html($this->template->render('task/edit', $params));
}
else {
- $this->response->html($this->taskLayout('task_edit', $params));
+ $this->response->html($this->taskLayout('task/edit', $params));
}
}
@@ -209,14 +227,14 @@ class Task extends Base
if ($valid) {
- if ($this->task->update($values)) {
+ if ($this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
if ($this->request->getIntegerParam('ajax')) {
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
else {
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']);
}
}
else {
@@ -224,20 +242,7 @@ class Task extends Base
}
}
- $this->response->html($this->taskLayout('task_edit', array(
- 'values' => $values,
- 'errors' => $errors,
- 'task' => $task,
- 'columns_list' => $this->board->getColumnsList($values['project_id']),
- 'users_list' => $this->projectPermission->getUsersList($values['project_id']),
- 'colors_list' => $this->color->getList(),
- 'categories_list' => $this->category->getList($values['project_id']),
- 'date_format' => $this->config->get('application_date_format'),
- 'date_formats' => $this->dateParser->getAvailableFormats(),
- 'menu' => 'tasks',
- 'title' => t('Edit a task'),
- 'ajax' => $this->request->isAjax(),
- )));
+ $this->edit($values, $errors);
}
/**
@@ -250,16 +255,16 @@ class Task extends Base
$task = $this->getTask();
$values = $this->request->getValues();
- list($valid, $errors) = $this->taskValidator->validateTimeModification($values);
+ list($valid,) = $this->taskValidator->validateTimeModification($values);
- if ($valid && $this->task->update($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('?controller=task&action=show&task_id='.$task['id']);
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']);
}
/**
@@ -270,24 +275,35 @@ class Task extends Base
public function close()
{
$task = $this->getTask();
+ $redirect = $this->request->getStringParam('redirect');
if ($this->request->getStringParam('confirmation') === 'yes') {
$this->checkCSRFParam();
- if ($this->task->close($task['id'])) {
+ if ($this->taskStatus->close($task['id'])) {
$this->session->flash(t('Task closed successfully.'));
} else {
$this->session->flashError(t('Unable to close this task.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
+ if ($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('task_close', array(
+ if ($this->request->isAjax()) {
+ $this->response->html($this->template->render('task/close', array(
+ 'task' => $task,
+ 'redirect' => $redirect,
+ )));
+ }
+
+ $this->response->html($this->taskLayout('task/close', array(
'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Close a task')
+ 'redirect' => $redirect,
)));
}
@@ -304,19 +320,17 @@ class Task extends Base
$this->checkCSRFParam();
- if ($this->task->open($task['id'])) {
+ if ($this->taskStatus->open($task['id'])) {
$this->session->flash(t('Task opened successfully.'));
} else {
$this->session->flashError(t('Unable to open this task.'));
}
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']);
}
- $this->response->html($this->taskLayout('task_open', array(
+ $this->response->html($this->taskLayout('task/open', array(
'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Open a task')
)));
}
@@ -346,10 +360,8 @@ class Task extends Base
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
- $this->response->html($this->taskLayout('task_remove', array(
+ $this->response->html($this->taskLayout('task/remove', array(
'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Remove a task')
)));
}
@@ -365,21 +377,19 @@ class Task extends Base
if ($this->request->getStringParam('confirmation') === 'yes') {
$this->checkCSRFParam();
- $task_id = $this->task->duplicateToSameProject($task);
+ $task_id = $this->taskDuplication->duplicate($task['id']);
if ($task_id) {
$this->session->flash(t('Task created successfully.'));
- $this->response->redirect('?controller=task&action=show&task_id='.$task_id);
+ $this->response->redirect('?controller=task&action=show&task_id='.$task_id.'&project_id='.$task['project_id']);
} else {
$this->session->flashError(t('Unable to create this task.'));
- $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id']);
+ $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id'].'&project_id='.$task['project_id']);
}
}
- $this->response->html($this->taskLayout('task_duplicate', array(
+ $this->response->html($this->taskLayout('task/duplicate', array(
'task' => $task,
- 'menu' => 'tasks',
- 'title' => t('Duplicate a task')
)));
}
@@ -401,7 +411,7 @@ class Task extends Base
if ($valid) {
- if ($this->task->update($values)) {
+ if ($this->taskModification->update($values)) {
$this->session->flash(t('Task updated successfully.'));
}
else {
@@ -412,7 +422,7 @@ class Task extends Base
$this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
}
else {
- $this->response->redirect('?controller=task&action=show&task_id='.$task['id']);
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']);
}
}
}
@@ -426,49 +436,123 @@ class Task extends Base
'errors' => $errors,
'task' => $task,
'ajax' => $ajax,
- 'menu' => 'tasks',
- 'title' => t('Edit the description'),
);
if ($ajax) {
- $this->response->html($this->template->load('task_edit_description', $params));
+ $this->response->html($this->template->render('task/edit_description', $params));
}
else {
- $this->response->html($this->taskLayout('task_edit_description', $params));
+ $this->response->html($this->taskLayout('task/edit_description', $params));
}
}
/**
- * Move a task to another project
+ * Edit recurrence form
*
* @access public
*/
- public function move()
+ public function recurrence()
{
- $this->toAnotherProject('move');
+ $task = $this->getTask();
+ $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
+
+ 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.'));
+ }
+
+ if ($ajax) {
+ $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']);
+ }
+ else {
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']);
+ }
+ }
+ }
+ else {
+ $values = $task;
+ $errors = array();
+ }
+
+ $params = array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'ajax' => $ajax,
+ 'recurrence_status_list' => $this->task->getRecurrenceStatusList(),
+ 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(),
+ 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(),
+ 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(),
+ );
+
+ if ($ajax) {
+ $this->response->html($this->template->render('task/edit_recurrence', $params));
+ }
+ else {
+ $this->response->html($this->taskLayout('task/edit_recurrence', $params));
+ }
}
/**
- * Duplicate a task to another project
+ * Move a task to another project
*
* @access public
*/
- public function copy()
+ public function move()
{
- $this->toAnotherProject('duplicate');
+ $task = $this->getTask();
+ $values = $task;
+ $errors = array();
+ $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
+
+ unset($projects_list[$task['project_id']]);
+
+ if ($this->request->isPost()) {
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->taskValidator->validateProjectModification($values);
+
+ if ($valid) {
+
+ if ($this->taskDuplication->moveToProject($task['id'], $values['project_id'])) {
+ $this->session->flash(t('Task updated successfully.'));
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$values['project_id']);
+ }
+ else {
+ $this->session->flashError(t('Unable to update your task.'));
+ }
+ }
+ }
+
+ $this->response->html($this->taskLayout('task/move_project', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'projects_list' => $projects_list,
+ )));
}
/**
- * Common methods between the actions "move" and "copy"
+ * Duplicate a task to another project
*
- * @access private
+ * @access public
*/
- private function toAnotherProject($action)
+ public function copy()
{
$task = $this->getTask();
$values = $task;
$errors = array();
- $projects_list = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
+ $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId());
unset($projects_list[$task['project_id']]);
@@ -478,10 +562,10 @@ class Task extends Base
list($valid, $errors) = $this->taskValidator->validateProjectModification($values);
if ($valid) {
- $task_id = $this->task->{$action.'ToAnotherProject'}($values['project_id'], $task);
+ $task_id = $this->taskDuplication->duplicateToProject($task['id'], $values['project_id']);
if ($task_id) {
$this->session->flash(t('Task created successfully.'));
- $this->response->redirect('?controller=task&action=show&task_id='.$task_id);
+ $this->response->redirect('?controller=task&action=show&task_id='.$task_id.'&project_id='.$values['project_id']);
}
else {
$this->session->flashError(t('Unable to create your task.'));
@@ -489,13 +573,49 @@ class Task extends Base
}
}
- $this->response->html($this->taskLayout('task_'.$action.'_project', array(
+ $this->response->html($this->taskLayout('task/duplicate_project', array(
'values' => $values,
'errors' => $errors,
'task' => $task,
'projects_list' => $projects_list,
- 'menu' => 'tasks',
- 'title' => t(ucfirst($action).' the task to another project')
+ )));
+ }
+
+ /**
+ * Display the time tracking details
+ *
+ * @access public
+ */
+ public function timesheet()
+ {
+ $task = $this->getTask();
+
+ $subtask_paginator = $this->paginator
+ ->setUrl('task', 'timesheet', 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', array(
+ 'task' => $task,
+ 'subtask_paginator' => $subtask_paginator,
+ )));
+ }
+
+ /**
+ * Display the task transitions
+ *
+ * @access public
+ */
+ public function transitions()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->taskLayout('task/transitions', array(
+ 'task' => $task,
+ 'transitions' => $this->transition->getAllByTask($task['id']),
)));
}
}
diff --git a/app/Controller/Tasklink.php b/app/Controller/Tasklink.php
new file mode 100644
index 00000000..dd076802
--- /dev/null
+++ b/app/Controller/Tasklink.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Controller;
+
+/**
+ * TaskLink controller
+ *
+ * @package controller
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class Tasklink extends Base
+{
+ /**
+ * Get the current link
+ *
+ * @access private
+ * @return array
+ */
+ private function getTaskLink()
+ {
+ $link = $this->taskLink->getById($this->request->getIntegerParam('link_id'));
+
+ if (empty($link)) {
+ $this->notfound();
+ }
+
+ return $link;
+ }
+
+ /**
+ * Creation form
+ *
+ * @access public
+ */
+ 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(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'labels' => $this->link->getList(0, false),
+ 'title' => t('Add a new link')
+ )));
+ }
+
+ /**
+ * Validation and creation
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+ $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax');
+
+ list($valid, $errors) = $this->taskLink->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');
+ }
+
+ $errors = array('title' => array(t('The exact same link already exists')));
+ $this->session->flashError(t('Unable to create your link.'));
+ }
+
+ $this->create($values, $errors);
+ }
+
+ /**
+ * Edit form
+ *
+ * @access public
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+ $task_link = $this->getTaskLink();
+
+ if (empty($values)) {
+ $opposite_task = $this->taskFinder->getById($task_link['opposite_task_id']);
+ $values = $task_link;
+ $values['title'] = '#'.$opposite_task['id'].' - '.$opposite_task['title'];
+ }
+
+ $this->response->html($this->taskLayout('tasklink/edit', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task_link' => $task_link,
+ 'task' => $task,
+ 'labels' => $this->link->getList(0, false),
+ 'title' => t('Edit link')
+ )));
+ }
+
+ /**
+ * Validation and update
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->taskLink->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->session->flashError(t('Unable to update your link.'));
+ }
+
+ $this->edit($values, $errors);
+ }
+
+ /**
+ * Confirmation dialog before removing a link
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $task = $this->getTask();
+ $link = $this->getTaskLink();
+
+ $this->response->html($this->taskLayout('tasklink/remove', array(
+ 'link' => $link,
+ 'task' => $task,
+ )));
+ }
+
+ /**
+ * Remove a link
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $task = $this->getTask();
+
+ if ($this->taskLink->remove($this->request->getIntegerParam('link_id'))) {
+ $this->session->flash(t('Link removed successfully.'));
+ }
+ else {
+ $this->session->flashError(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/Timetable.php b/app/Controller/Timetable.php
new file mode 100644
index 00000000..65edb44c
--- /dev/null
+++ b/app/Controller/Timetable.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Controller;
+
+use DateTime;
+
+/**
+ * Timetable controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Timetable extends User
+{
+ /**
+ * Display timetable for the user
+ *
+ * @access public
+ */
+ public function index()
+ {
+ $user = $this->getUser();
+ $from = $this->request->getStringParam('from', date('Y-m-d'));
+ $to = $this->request->getStringParam('to', date('Y-m-d', strtotime('next week')));
+ $timetable = $this->timetable->calculate($user['id'], new DateTime($from), new DateTime($to));
+
+ $this->response->html($this->layout('timetable/index', array(
+ 'user' => $user,
+ 'timetable' => $timetable,
+ 'values' => array(
+ 'from' => $from,
+ 'to' => $to,
+ 'controller' => 'timetable',
+ 'action' => 'index',
+ 'user_id' => $user['id'],
+ ),
+ )));
+ }
+}
diff --git a/app/Controller/Timetableday.php b/app/Controller/Timetableday.php
new file mode 100644
index 00000000..c8f7ac8a
--- /dev/null
+++ b/app/Controller/Timetableday.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Day Timetable controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Timetableday extends User
+{
+ /**
+ * Display timetable for the user
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('timetable_day/index', array(
+ 'timetable' => $this->timetableDay->getByUser($user['id']),
+ 'values' => $values + array('user_id' => $user['id']),
+ 'errors' => $errors,
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Validate and save
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->timetableDay->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->timetableDay->create($values['user_id'], $values['start'], $values['end'])) {
+ $this->session->flash(t('Time slot created successfully.'));
+ $this->response->redirect($this->helper->url->to('timetableday', 'index', array('user_id' => $values['user_id'])));
+ }
+ else {
+ $this->session->flashError(t('Unable to save this time slot.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Confirmation dialag box to remove a row
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('timetable_day/remove', array(
+ 'slot_id' => $this->request->getIntegerParam('slot_id'),
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Remove a row
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $user = $this->getUser();
+
+ if ($this->timetableDay->remove($this->request->getIntegerParam('slot_id'))) {
+ $this->session->flash(t('Time slot removed successfully.'));
+ }
+ else {
+ $this->session->flash(t('Unable to remove this time slot.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('timetableday', 'index', array('user_id' => $user['id'])));
+ }
+}
diff --git a/app/Controller/Timetableextra.php b/app/Controller/Timetableextra.php
new file mode 100644
index 00000000..7c6fe265
--- /dev/null
+++ b/app/Controller/Timetableextra.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Over-time Timetable controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Timetableextra extends Timetableoff
+{
+ protected $model = 'timetableExtra';
+ protected $controller_url = 'timetableextra';
+ protected $template_dir = 'timetable_extra';
+}
diff --git a/app/Controller/Timetableoff.php b/app/Controller/Timetableoff.php
new file mode 100644
index 00000000..585014a3
--- /dev/null
+++ b/app/Controller/Timetableoff.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Time-off Timetable controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Timetableoff extends User
+{
+ protected $model = 'timetableOff';
+ protected $controller_url = 'timetableoff';
+ protected $template_dir = 'timetable_off';
+
+ /**
+ * Display timetable for the user
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $user = $this->getUser();
+
+ $paginator = $this->paginator
+ ->setUrl($this->controller_url, 'index', array('user_id' => $user['id']))
+ ->setMax(10)
+ ->setOrder('date')
+ ->setDirection('desc')
+ ->setQuery($this->{$this->model}->getUserQuery($user['id']))
+ ->calculate();
+
+ $this->response->html($this->layout($this->template_dir.'/index', array(
+ 'values' => $values + array('user_id' => $user['id']),
+ 'errors' => $errors,
+ 'paginator' => $paginator,
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Validate and save
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->{$this->model}->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->{$this->model}->create(
+ $values['user_id'],
+ $values['date'],
+ isset($values['all_day']) && $values['all_day'] == 1,
+ $values['start'],
+ $values['end'],
+ $values['comment'])) {
+
+ $this->session->flash(t('Time slot created successfully.'));
+ $this->response->redirect($this->helper->url->to($this->controller_url, 'index', array('user_id' => $values['user_id'])));
+ }
+ else {
+ $this->session->flashError(t('Unable to save this time slot.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Confirmation dialag box to remove a row
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout($this->template_dir.'/remove', array(
+ 'slot_id' => $this->request->getIntegerParam('slot_id'),
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Remove a row
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $user = $this->getUser();
+
+ if ($this->{$this->model}->remove($this->request->getIntegerParam('slot_id'))) {
+ $this->session->flash(t('Time slot removed successfully.'));
+ }
+ else {
+ $this->session->flash(t('Unable to remove this time slot.'));
+ }
+
+ $this->response->redirect($this->helper->url->to($this->controller_url, 'index', array('user_id' => $user['id'])));
+ }
+}
diff --git a/app/Controller/Timetableweek.php b/app/Controller/Timetableweek.php
new file mode 100644
index 00000000..b8ce00e7
--- /dev/null
+++ b/app/Controller/Timetableweek.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Week Timetable controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Timetableweek extends User
+{
+ /**
+ * Display timetable for the user
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $user = $this->getUser();
+
+ if (empty($values)) {
+
+ $day = $this->timetableDay->getByUser($user['id']);
+
+ $values = array(
+ 'user_id' => $user['id'],
+ 'start' => isset($day[0]['start']) ? $day[0]['start'] : null,
+ 'end' => isset($day[0]['end']) ? $day[0]['end'] : null,
+ );
+ }
+
+ $this->response->html($this->layout('timetable_week/index', array(
+ 'timetable' => $this->timetableWeek->getByUser($user['id']),
+ 'values' => $values,
+ 'errors' => $errors,
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Validate and save
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->timetableWeek->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->timetableWeek->create($values['user_id'], $values['day'], $values['start'], $values['end'])) {
+ $this->session->flash(t('Time slot created successfully.'));
+ $this->response->redirect($this->helper->url->to('timetableweek', 'index', array('user_id' => $values['user_id'])));
+ }
+ else {
+ $this->session->flashError(t('Unable to save this time slot.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Confirmation dialag box to remove a row
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('timetable_week/remove', array(
+ 'slot_id' => $this->request->getIntegerParam('slot_id'),
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Remove a row
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $user = $this->getUser();
+
+ if ($this->timetableWeek->remove($this->request->getIntegerParam('slot_id'))) {
+ $this->session->flash(t('Time slot removed successfully.'));
+ }
+ else {
+ $this->session->flash(t('Unable to remove this time slot.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('timetableweek', 'index', array('user_id' => $user['id'])));
+ }
+}
diff --git a/app/Controller/Twofactor.php b/app/Controller/Twofactor.php
new file mode 100644
index 00000000..a8b0351f
--- /dev/null
+++ b/app/Controller/Twofactor.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace Controller;
+
+use Otp\Otp;
+use Otp\GoogleAuthenticator;
+use Base32\Base32;
+
+/**
+ * Two Factor Auth controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Twofactor extends User
+{
+ /**
+ * Only the current user can access to 2FA settings
+ *
+ * @access private
+ */
+ private function checkCurrentUser(array $user)
+ {
+ if ($user['id'] != $this->userSession->getId()) {
+ $this->forbidden();
+ }
+ }
+
+ /**
+ * Index
+ *
+ * @access public
+ */
+ public function index()
+ {
+ $user = $this->getUser();
+ $this->checkCurrentUser($user);
+
+ $label = $user['email'] ?: $user['username'];
+
+ $this->response->html($this->layout('twofactor/index', 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']) : '',
+ )));
+ }
+
+ /**
+ * Enable/disable 2FA
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $user = $this->getUser();
+ $this->checkCurrentUser($user);
+
+ $values = $this->request->getValues();
+
+ if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) {
+ $this->user->update(array(
+ 'id' => $user['id'],
+ 'twofactor_activated' => 1,
+ 'twofactor_secret' => GoogleAuthenticator::generateRandom(),
+ ));
+ }
+ 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;
+
+ $this->session->flash(t('User updated successfully.'));
+ $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
+ }
+
+ /**
+ * Test 2FA
+ *
+ * @access public
+ */
+ public function test()
+ {
+ $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->flash(t('The two factor authentication code is valid.'));
+ }
+ else {
+ $this->session->flashError(t('The two factor authentication code is not valid.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id'])));
+ }
+
+ /**
+ * Check 2FA
+ *
+ * @access public
+ */
+ public function check()
+ {
+ $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.'));
+ $this->response->redirect($this->helper->url->to('app', 'index'));
+ }
+ else {
+ $this->session->flashError(t('The two factor authentication code is not valid.'));
+ $this->response->redirect($this->helper->url->to('twofactor', 'code'));
+ }
+ }
+
+ /**
+ * Ask the 2FA code
+ *
+ * @access public
+ */
+ public function code()
+ {
+ $this->response->html($this->template->layout('twofactor/check', array(
+ 'title' => t('Check two factor authentication code'),
+ )));
+ }
+
+ /**
+ * Disable 2FA for a user
+ *
+ * @access public
+ */
+ public function disable()
+ {
+ $user = $this->getUser();
+
+ if ($this->request->getStringParam('disable') === 'yes') {
+
+ $this->checkCSRFParam();
+
+ $this->user->update(array(
+ 'id' => $user['id'],
+ 'twofactor_activated' => 0,
+ 'twofactor_secret' => '',
+ ));
+
+ $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id'])));
+ }
+
+ $this->response->html($this->layout('twofactor/disable', array(
+ 'user' => $user,
+ )));
+ }
+}
diff --git a/app/Controller/User.php b/app/Controller/User.php
index 834b2379..4cea06b1 100644
--- a/app/Controller/User.php
+++ b/app/Controller/User.php
@@ -11,103 +11,41 @@ namespace Controller;
class User extends Base
{
/**
- * Logout and destroy session
- *
- * @access public
- */
- public function logout()
- {
- $this->checkCSRFParam();
- $this->authentication->backend('rememberMe')->destroy($this->acl->getUserId());
- $this->session->close();
- $this->response->redirect('?controller=user&action=login');
- }
-
- /**
- * Display the form login
- *
- * @access public
- */
- public function login()
- {
- if ($this->acl->isLogged()) {
- $this->response->redirect('?controller=app');
- }
-
- $this->response->html($this->template->layout('user_login', array(
- 'errors' => array(),
- 'values' => array(),
- 'no_layout' => true,
- 'redirect_query' => $this->request->getStringParam('redirect_query'),
- 'title' => t('Login')
- )));
- }
-
- /**
- * Check credentials
- *
- * @access public
- */
- public function check()
- {
- $redirect_query = $this->request->getStringParam('redirect_query');
- $values = $this->request->getValues();
- list($valid, $errors) = $this->authentication->validateForm($values);
-
- if ($valid) {
- if ($redirect_query !== '') {
- $this->response->redirect('?'.$redirect_query);
- }
- else {
- $this->response->redirect('?controller=app');
- }
- }
-
- $this->response->html($this->template->layout('user_login', array(
- 'errors' => $errors,
- 'values' => $values,
- 'no_layout' => true,
- 'redirect_query' => $redirect_query,
- 'title' => t('Login')
- )));
- }
-
- /**
* Common layout for user views
*
- * @access private
+ * @access protected
* @param string $template Template name
* @param array $params Template parameters
* @return string
*/
- private function layout($template, array $params)
+ protected function layout($template, array $params)
{
- $content = $this->template->load($template, $params);
+ $content = $this->template->render($template, $params);
$params['user_content_for_layout'] = $content;
- $params['menu'] = 'users';
+ $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId());
if (isset($params['user'])) {
- $params['title'] = $params['user']['name'] ?: $params['user']['username'];
+ $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')';
}
- return $this->template->layout('user_layout', $params);
+ return $this->template->layout('user/layout', $params);
}
/**
* Common method to get the user
*
- * @access private
+ * @access protected
* @return array
*/
- private function getUser()
+ protected function getUser()
{
$user = $this->user->getById($this->request->getIntegerParam('user_id'));
- if (! $user) {
+ if (empty($user)) {
$this->notfound();
}
- if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) {
+ if (! $this->userSession->isAdmin() && $this->userSession->getId() != $user['id']) {
$this->forbidden();
}
@@ -121,16 +59,19 @@ class User extends Base
*/
public function index()
{
- $users = $this->user->getAll();
- $nb_users = count($users);
+ $paginator = $this->paginator
+ ->setUrl('user', 'index')
+ ->setMax(30)
+ ->setOrder('username')
+ ->setQuery($this->user->getQuery())
+ ->calculate();
$this->response->html(
- $this->template->layout('user_index', array(
+ $this->template->layout('user/index', array(
+ 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'projects' => $this->project->getList(),
- 'users' => $users,
- 'nb_users' => $nb_users,
- 'menu' => 'users',
- 'title' => t('Users').' ('.$nb_users.')'
+ 'title' => t('Users').' ('.$paginator->getTotal().')',
+ 'paginator' => $paginator,
)));
}
@@ -139,13 +80,15 @@ class User extends Base
*
* @access public
*/
- public function create()
+ public function create(array $values = array(), array $errors = array())
{
- $this->response->html($this->template->layout('user_new', array(
+ $this->response->html($this->template->layout('user/new', array(
+ 'timezones' => $this->config->getTimezones(true),
+ 'languages' => $this->config->getLanguages(true),
+ 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()),
'projects' => $this->project->getList(),
- 'errors' => array(),
- 'values' => array(),
- 'menu' => 'users',
+ 'errors' => $errors,
+ 'values' => $values,
'title' => t('New user')
)));
}
@@ -162,22 +105,18 @@ class User extends Base
if ($valid) {
- if ($this->user->create($values)) {
+ $user_id = $this->user->create($values);
+
+ if ($user_id !== false) {
$this->session->flash(t('User created successfully.'));
- $this->response->redirect('?controller=user');
+ $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->response->html($this->template->layout('user_new', array(
- 'projects' => $this->project->getList(),
- 'errors' => $errors,
- 'values' => $values,
- 'menu' => 'users',
- 'title' => t('New user')
- )));
+ $this->create($values, $errors);
}
/**
@@ -188,9 +127,48 @@ class User extends Base
public function show()
{
$user = $this->getUser();
- $this->response->html($this->layout('user_show', array(
+ $this->response->html($this->layout('user/show', array(
'projects' => $this->projectPermission->getAllowedProjects($user['id']),
'user' => $user,
+ 'timezones' => $this->config->getTimezones(true),
+ 'languages' => $this->config->getLanguages(true),
+ )));
+ }
+
+ /**
+ * Display user calendar
+ *
+ * @access public
+ */
+ public function calendar()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('user/calendar', array(
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Display timesheet
+ *
+ * @access public
+ */
+ public function timesheet()
+ {
+ $user = $this->getUser();
+
+ $subtask_paginator = $this->paginator
+ ->setUrl('user', 'timesheet', array('user_id' => $user['id'], 'pagination' => 'subtasks'))
+ ->setMax(20)
+ ->setOrder('start')
+ ->setDirection('DESC')
+ ->setQuery($this->subtaskTimeTracking->getUserQuery($user['id']))
+ ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks');
+
+ $this->response->html($this->layout('user/timesheet', array(
+ 'subtask_paginator' => $subtask_paginator,
+ 'user' => $user,
)));
}
@@ -202,7 +180,7 @@ class User extends Base
public function last()
{
$user = $this->getUser();
- $this->response->html($this->layout('user_last', array(
+ $this->response->html($this->layout('user/last', array(
'last_logins' => $this->lastLogin->getAll($user['id']),
'user' => $user,
)));
@@ -216,7 +194,7 @@ class User extends Base
public function sessions()
{
$user = $this->getUser();
- $this->response->html($this->layout('user_sessions', array(
+ $this->response->html($this->layout('user/sessions', array(
'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']),
'user' => $user,
)));
@@ -251,7 +229,7 @@ class User extends Base
$this->response->redirect('?controller=user&action=notifications&user_id='.$user['id']);
}
- $this->response->html($this->layout('user_notifications', array(
+ $this->response->html($this->layout('user/notifications', array(
'projects' => $this->projectPermission->getAllowedProjects($user['id']),
'notifications' => $this->notification->readSettings($user['id']),
'user' => $user,
@@ -266,13 +244,42 @@ class User extends Base
public function external()
{
$user = $this->getUser();
- $this->response->html($this->layout('user_external', array(
+ $this->response->html($this->layout('user/external', array(
'last_logins' => $this->lastLogin->getAll($user['id']),
'user' => $user,
)));
}
/**
+ * Public access management
+ *
+ * @access public
+ */
+ public function share()
+ {
+ $user = $this->getUser();
+ $switch = $this->request->getStringParam('switch');
+
+ if ($switch === 'enable' || $switch === 'disable') {
+
+ $this->checkCSRFParam();
+
+ if ($this->user->{$switch.'PublicAccess'}($user['id'])) {
+ $this->session->flash(t('User updated successfully.'));
+ } else {
+ $this->session->flashError(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(
+ 'user' => $user,
+ 'title' => t('Public access'),
+ )));
+ }
+
+ /**
* Password modification
*
* @access public
@@ -301,7 +308,7 @@ class User extends Base
}
}
- $this->response->html($this->layout('user_password', array(
+ $this->response->html($this->layout('user/password', array(
'values' => $values,
'errors' => $errors,
'user' => $user,
@@ -323,9 +330,9 @@ class User extends Base
if ($this->request->isPost()) {
- $values = $this->request->getValues();
+ $values = $this->request->getValues() + array('disable_login_form' => 0);
- if ($this->acl->isAdminUser()) {
+ if ($this->userSession->isAdmin()) {
$values += array('is_admin' => 0);
}
else {
@@ -350,11 +357,13 @@ class User extends Base
}
}
- $this->response->html($this->layout('user_edit', array(
+ $this->response->html($this->layout('user/edit', array(
'values' => $values,
'errors' => $errors,
'projects' => $this->projectPermission->filterProjects($this->project->getList(), $user['id']),
'user' => $user,
+ 'timezones' => $this->config->getTimezones(true),
+ 'languages' => $this->config->getLanguages(true),
)));
}
@@ -380,7 +389,7 @@ class User extends Base
$this->response->redirect('?controller=user');
}
- $this->response->html($this->layout('user_remove', array(
+ $this->response->html($this->layout('user/remove', array(
'user' => $user,
)));
}
@@ -401,22 +410,22 @@ class User extends Base
if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate
- if ($this->acl->isLogged()) {
+ if ($this->userSession->isLogged()) {
- if ($this->authentication->backend('google')->updateUser($this->acl->getUserId(), $profile)) {
+ if ($this->authentication->backend('google')->updateUser($this->userSession->getId(), $profile)) {
$this->session->flash(t('Your Google Account is linked to your profile successfully.'));
}
else {
$this->session->flashError(t('Unable to link your Google Account.'));
}
- $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId());
+ $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId());
}
else if ($this->authentication->backend('google')->authenticate($profile['id'])) {
$this->response->redirect('?controller=app');
}
else {
- $this->response->html($this->template->layout('user_login', array(
+ $this->response->html($this->template->layout('auth/index', array(
'errors' => array('login' => t('Google authentication failed')),
'values' => array(),
'no_layout' => true,
@@ -438,14 +447,14 @@ class User extends Base
public function unlinkGoogle()
{
$this->checkCSRFParam();
- if ($this->authentication->backend('google')->unlink($this->acl->getUserId())) {
+ if ($this->authentication->backend('google')->unlink($this->userSession->getId())) {
$this->session->flash(t('Your Google Account is not linked anymore to your profile.'));
}
else {
$this->session->flashError(t('Unable to unlink your Google Account.'));
}
- $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId());
+ $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId());
}
/**
@@ -453,7 +462,7 @@ class User extends Base
*
* @access public
*/
- public function gitHub()
+ public function github()
{
$code = $this->request->getStringParam('code');
@@ -463,22 +472,22 @@ class User extends Base
if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate
- if ($this->acl->isLogged()) {
+ if ($this->userSession->isLogged()) {
- if ($this->authentication->backend('gitHub')->updateUser($this->acl->getUserId(), $profile)) {
+ if ($this->authentication->backend('gitHub')->updateUser($this->userSession->getId(), $profile)) {
$this->session->flash(t('Your GitHub account was successfully linked to your profile.'));
}
else {
$this->session->flashError(t('Unable to link your GitHub Account.'));
}
- $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId());
+ $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId());
}
else if ($this->authentication->backend('gitHub')->authenticate($profile['id'])) {
$this->response->redirect('?controller=app');
}
else {
- $this->response->html($this->template->layout('user_login', array(
+ $this->response->html($this->template->layout('auth/index', array(
'errors' => array('login' => t('GitHub authentication failed')),
'values' => array(),
'no_layout' => true,
@@ -497,19 +506,19 @@ class User extends Base
*
* @access public
*/
- public function unlinkGitHub()
+ public function unlinkGithub()
{
$this->checkCSRFParam();
$this->authentication->backend('gitHub')->revokeGitHubAccess();
- if ($this->authentication->backend('gitHub')->unlink($this->acl->getUserId())) {
+ if ($this->authentication->backend('gitHub')->unlink($this->userSession->getId())) {
$this->session->flash(t('Your GitHub account is no longer linked to your profile.'));
}
else {
$this->session->flashError(t('Unable to unlink your GitHub Account.'));
}
- $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId());
+ $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId());
}
}
diff --git a/app/Controller/Webhook.php b/app/Controller/Webhook.php
index 71acab08..10a24e47 100644
--- a/app/Controller/Webhook.php
+++ b/app/Controller/Webhook.php
@@ -35,7 +35,7 @@ class Webhook extends Base
list($valid,) = $this->taskValidator->validateCreation($values);
- if ($valid && $this->task->create($values)) {
+ if ($valid && $this->taskCreation->create($values)) {
$this->response->text('OK');
}
@@ -55,9 +55,91 @@ class Webhook extends Base
$this->githubWebhook->setProjectId($this->request->getIntegerParam('project_id'));
- $this->githubWebhook->parsePayload(
+ $result = $this->githubWebhook->parsePayload(
$this->request->getHeader('X-Github-Event'),
- $this->request->getBody()
+ $this->request->getJson() ?: array()
);
+
+ echo $result ? 'PARSED' : 'IGNORED';
+ }
+
+ /**
+ * Handle Gitlab webhooks
+ *
+ * @access public
+ */
+ public function gitlab()
+ {
+ if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ $this->gitlabWebhook->setProjectId($this->request->getIntegerParam('project_id'));
+
+ $result = $this->gitlabWebhook->parsePayload(
+ $this->request->getJson() ?: array()
+ );
+
+ echo $result ? 'PARSED' : 'IGNORED';
+ }
+
+ /**
+ * Handle Bitbucket webhooks
+ *
+ * @access public
+ */
+ public function bitbucket()
+ {
+ if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ $this->bitbucketWebhook->setProjectId($this->request->getIntegerParam('project_id'));
+
+ $result = $this->bitbucketWebhook->parsePayload(json_decode(@$_POST['payload'], true) ?: array());
+
+ echo $result ? 'PARSED' : 'IGNORED';
+ }
+
+ /**
+ * Handle Postmark webhooks
+ *
+ * @access public
+ */
+ public function postmark()
+ {
+ if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ echo $this->postmark->receiveEmail($this->request->getJson() ?: array()) ? 'PARSED' : 'IGNORED';
+ }
+
+ /**
+ * Handle Mailgun webhooks
+ *
+ * @access public
+ */
+ public function mailgun()
+ {
+ if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ echo $this->mailgun->receiveEmail($_POST) ? 'PARSED' : 'IGNORED';
+ }
+
+ /**
+ * Handle Sendgrid webhooks
+ *
+ * @access public
+ */
+ public function sendgrid()
+ {
+ if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ echo $this->sendgridWebhook->parsePayload($_POST) ? 'PARSED' : 'IGNORED';
}
}
diff --git a/app/Core/Base.php b/app/Core/Base.php
new file mode 100644
index 00000000..6cb87cbc
--- /dev/null
+++ b/app/Core/Base.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Core;
+
+use Pimple\Container;
+
+/**
+ * Base class
+ *
+ * @package core
+ * @author Frederic Guillot
+ *
+ * @property \Core\Helper $helper
+ * @property \Core\EmailClient $emailClient
+ * @property \Core\HttpClient $httpClient
+ * @property \Core\Paginator $paginator
+ * @property \Core\Request $request
+ * @property \Core\Session $session
+ * @property \Core\Template $template
+ * @property \Integration\BitbucketWebhook $bitbucketWebhook
+ * @property \Integration\GithubWebhook $githubWebhook
+ * @property \Integration\GitlabWebhook $gitlabWebhook
+ * @property \Integration\HipchatWebhook $hipchatWebhook
+ * @property \Integration\Jabber $jabber
+ * @property \Integration\Mailgun $mailgun
+ * @property \Integration\Postmark $postmark
+ * @property \Integration\SendgridWebhook $sendgridWebhook
+ * @property \Integration\SlackWebhook $slackWebhook
+ * @property \Integration\Smtp $smtp
+ * @property \Model\Acl $acl
+ * @property \Model\Action $action
+ * @property \Model\Authentication $authentication
+ * @property \Model\Board $board
+ * @property \Model\Budget $budget
+ * @property \Model\Category $category
+ * @property \Model\Color $color
+ * @property \Model\Comment $comment
+ * @property \Model\Config $config
+ * @property \Model\Currency $currency
+ * @property \Model\DateParser $dateParser
+ * @property \Model\File $file
+ * @property \Model\HourlyRate $hourlyRate
+ * @property \Model\LastLogin $lastLogin
+ * @property \Model\Link $link
+ * @property \Model\Notification $notification
+ * @property \Model\Project $project
+ * @property \Model\ProjectActivity $projectActivity
+ * @property \Model\ProjectAnalytic $projectAnalytic
+ * @property \Model\ProjectDuplication $projectDuplication
+ * @property \Model\ProjectDailySummary $projectDailySummary
+ * @property \Model\ProjectIntegration $projectIntegration
+ * @property \Model\ProjectPermission $projectPermission
+ * @property \Model\Subtask $subtask
+ * @property \Model\SubtaskExport $subtaskExport
+ * @property \Model\SubtaskForecast $subtaskForecast
+ * @property \Model\SubtaskTimeTracking $subtaskTimeTracking
+ * @property \Model\Swimlane $swimlane
+ * @property \Model\Task $task
+ * @property \Model\TaskCreation $taskCreation
+ * @property \Model\TaskDuplication $taskDuplication
+ * @property \Model\TaskExport $taskExport
+ * @property \Model\TaskFinder $taskFinder
+ * @property \Model\TaskFilter $taskFilter
+ * @property \Model\TaskLink $taskLink
+ * @property \Model\TaskModification $taskModification
+ * @property \Model\TaskPermission $taskPermission
+ * @property \Model\TaskPosition $taskPosition
+ * @property \Model\TaskStatus $taskStatus
+ * @property \Model\TaskValidator $taskValidator
+ * @property \Model\Timetable $timetable
+ * @property \Model\TimetableDay $timetableDay
+ * @property \Model\TimetableExtra $timetableExtra
+ * @property \Model\TimetableOff $timetableOff
+ * @property \Model\TimetableWeek $timetableWeek
+ * @property \Model\Transition $transition
+ * @property \Model\User $user
+ * @property \Model\UserSession $userSession
+ * @property \Model\Webhook $webhook
+ */
+abstract class Base
+{
+ /**
+ * Container instance
+ *
+ * @access protected
+ * @var \Pimple\Container
+ */
+ protected $container;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ */
+ public function __construct(Container $container)
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * Load automatically models
+ *
+ * @access public
+ * @param string $name Model name
+ * @return mixed
+ */
+ public function __get($name)
+ {
+ return $this->container[$name];
+ }
+}
diff --git a/app/Core/Cache.php b/app/Core/Cache.php
new file mode 100644
index 00000000..670a76e0
--- /dev/null
+++ b/app/Core/Cache.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Core;
+
+use Pimple\Container;
+
+abstract class Cache
+{
+ /**
+ * Container instance
+ *
+ * @access protected
+ * @var \Pimple\Container
+ */
+ protected $container;
+
+ abstract public function init();
+ abstract public function set($key, $value);
+ abstract public function get($key);
+ abstract public function flush();
+ abstract public function remove($key);
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ */
+ public function __construct(Container $container)
+ {
+ $this->container = $container;
+ $this->init();
+ }
+
+ /**
+ * Proxy cache
+ *
+ * Note: Arguments must be scalar types
+ *
+ * @access public
+ * @param string $container Container name
+ * @param string $method Container method
+ * @return mixed
+ */
+ public function proxy($container, $method)
+ {
+ $args = func_get_args();
+ $key = 'proxy_'.implode('_', $args);
+ $result = $this->get($key);
+
+ if ($result === null) {
+ $result = call_user_func_array(array($this->container[$container], $method), array_splice($args, 2));
+ $this->set($key, $result);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/Core/Cli.php b/app/Core/Cli.php
deleted file mode 100644
index 13533b9a..00000000
--- a/app/Core/Cli.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-namespace Core;
-
-use Closure;
-
-/**
- * CLI class
- *
- * @package core
- * @author Frederic Guillot
- */
-class Cli
-{
- /**
- * Default command name
- *
- * @access public
- * @var string
- */
- public $default_command = 'help';
-
- /**
- * List of registered commands
- *
- * @access private
- * @var array
- */
- private $commands = array();
-
- /**
- *
- *
- * @access public
- * @param string $command Command name
- * @param Closure $callback Command callback
- */
- public function register($command, Closure $callback)
- {
- $this->commands[$command] = $callback;
- }
-
- /**
- * Execute a command
- *
- * @access public
- * @param string $command Command name
- */
- public function call($command)
- {
- if (isset($this->commands[$command])) {
- $this->commands[$command]();
- exit;
- }
- }
-
- /**
- * Determine which command to execute
- *
- * @access public
- */
- public function execute()
- {
- if (php_sapi_name() !== 'cli') {
- die('This script work only from the command line.');
- }
-
- if ($GLOBALS['argc'] === 1) {
- $this->call($this->default_command);
- }
-
- $this->call($GLOBALS['argv'][1]);
- $this->call($this->default_command);
- }
-}
diff --git a/app/Core/EmailClient.php b/app/Core/EmailClient.php
new file mode 100644
index 00000000..07687c42
--- /dev/null
+++ b/app/Core/EmailClient.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Core;
+
+/**
+ * Mail client
+ *
+ * @package core
+ * @author Frederic Guillot
+ */
+class EmailClient extends Base
+{
+ /**
+ * Send a HTML email
+ *
+ * @access public
+ * @param string $email
+ * @param string $name
+ * @param string $subject
+ * @param string $html
+ */
+ public function send($email, $name, $subject, $html)
+ {
+ $this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
+
+ $start_time = microtime(true);
+ $author = 'Kanboard';
+
+ if (Session::isOpen() && $this->userSession->isLogged()) {
+ $author = e('%s via Kanboard', $this->user->getFullname($this->session['user']));
+ }
+
+ switch (MAIL_TRANSPORT) {
+ case 'mailgun':
+ $this->mailgun->sendEmail($email, $name, $subject, $html, $author);
+ break;
+ case 'postmark':
+ $this->postmark->sendEmail($email, $name, $subject, $html, $author);
+ break;
+ default:
+ $this->smtp->sendEmail($email, $name, $subject, $html, $author);
+ }
+
+ $this->container['logger']->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
+ }
+}
diff --git a/app/Core/Event.php b/app/Core/Event.php
deleted file mode 100644
index a32499d8..00000000
--- a/app/Core/Event.php
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-
-namespace Core;
-
-/**
- * Event dispatcher class
- *
- * @package core
- * @author Frederic Guillot
- */
-class Event
-{
- /**
- * Contains all listeners
- *
- * @access private
- * @var array
- */
- private $listeners = array();
-
- /**
- * The last listener executed
- *
- * @access private
- * @var string
- */
- private $lastListener = '';
-
- /**
- * The last triggered event
- *
- * @access private
- * @var string
- */
- private $lastEvent = '';
-
- /**
- * Triggered events list
- *
- * @access private
- * @var array
- */
- private $events = array();
-
- /**
- * Attach a listener object to an event
- *
- * @access public
- * @param string $eventName Event name
- * @param Listener $listener Object that implements the Listener interface
- */
- public function attach($eventName, Listener $listener)
- {
- if (! isset($this->listeners[$eventName])) {
- $this->listeners[$eventName] = array();
- }
-
- $this->listeners[$eventName][] = $listener;
- }
-
- /**
- * Trigger an event
- *
- * @access public
- * @param string $eventName Event name
- * @param array $data Event data
- */
- public function trigger($eventName, array $data)
- {
- if (! $this->isEventTriggered($eventName)) {
-
- $this->events[] = $eventName;
-
- if (isset($this->listeners[$eventName])) {
-
- foreach ($this->listeners[$eventName] as $listener) {
-
- $this->lastEvent = $eventName;
-
- if ($listener->execute($data)) {
- $this->lastListener = get_class($listener);
- }
- }
- }
- }
- }
-
- /**
- * Get the last listener executed
- *
- * @access public
- * @return string Event name
- */
- public function getLastListenerExecuted()
- {
- return $this->lastListener;
- }
-
- /**
- * Get the last fired event
- *
- * @access public
- * @return string Event name
- */
- public function getLastTriggeredEvent()
- {
- return $this->lastEvent;
- }
-
- /**
- * Get a list of triggered events
- *
- * @access public
- * @return array
- */
- public function getTriggeredEvents()
- {
- return $this->events;
- }
-
- /**
- * Check if an event have been triggered
- *
- * @access public
- * @param string $eventName Event name
- * @return bool
- */
- public function isEventTriggered($eventName)
- {
- return in_array($eventName, $this->events);
- }
-
- /**
- * Flush the list of triggered events
- *
- * @access public
- */
- public function clearTriggeredEvents()
- {
- $this->events = array();
- $this->lastEvent = '';
- }
-
- /**
- * Check if a listener bind to an event
- *
- * @access public
- * @param string $eventName Event name
- * @param mixed $instance Instance name or object itself
- * @return bool Yes or no
- */
- public function hasListener($eventName, $instance)
- {
- if (isset($this->listeners[$eventName])) {
- foreach ($this->listeners[$eventName] as $listener) {
- if ($listener instanceof $instance) {
- return true;
- }
- }
- }
-
- return false;
- }
-}
diff --git a/app/Core/Helper.php b/app/Core/Helper.php
new file mode 100644
index 00000000..53084a7e
--- /dev/null
+++ b/app/Core/Helper.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Core;
+
+/**
+ * Helper base class
+ *
+ * @package core
+ * @author Frederic Guillot
+ *
+ * @property \Helper\App $app
+ * @property \Helper\Asset $asset
+ * @property \Helper\Datetime $datetime
+ * @property \Helper\File $file
+ * @property \Helper\Form $form
+ * @property \Helper\Subtask $subtask
+ * @property \Helper\Task $task
+ * @property \Helper\Text $text
+ * @property \Helper\Url $url
+ * @property \Helper\User $user
+ */
+class Helper extends Base
+{
+ /**
+ * Helper instances
+ *
+ * @static
+ * @access private
+ * @var array
+ */
+ private static $helpers = array();
+
+ /**
+ * Load automatically helpers
+ *
+ * @access public
+ * @param string $name Helper name
+ * @return mixed
+ */
+ public function __get($name)
+ {
+ if (! isset(self::$helpers[$name])) {
+ $class = '\Helper\\'.ucfirst($name);
+ self::$helpers[$name] = new $class($this->container);
+ }
+
+ return self::$helpers[$name];
+ }
+
+ /**
+ * HTML escaping
+ *
+ * @param string $value Value to escape
+ * @return string
+ */
+ public function e($value)
+ {
+ return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
+ }
+}
diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php
new file mode 100644
index 00000000..2f280a1e
--- /dev/null
+++ b/app/Core/HttpClient.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Core;
+
+/**
+ * HTTP client
+ *
+ * @package core
+ * @author Frederic Guillot
+ */
+class HttpClient extends Base
+{
+ /**
+ * HTTP connection timeout in seconds
+ *
+ * @var integer
+ */
+ const HTTP_TIMEOUT = 5;
+
+ /**
+ * Number of maximum redirections for the HTTP client
+ *
+ * @var integer
+ */
+ const HTTP_MAX_REDIRECTS = 2;
+
+ /**
+ * HTTP client user agent
+ *
+ * @var string
+ */
+ const HTTP_USER_AGENT = 'Kanboard';
+
+ /**
+ * Send a POST HTTP request encoded in JSON
+ *
+ * @access public
+ * @param string $url
+ * @param array $data
+ * @param array $headers
+ * @return string
+ */
+ public function postJson($url, array $data, array $headers = array())
+ {
+ return $this->doRequest(
+ $url,
+ json_encode($data),
+ array_merge(array('Content-type: application/json'), $headers)
+ );
+ }
+
+ /**
+ * Send a POST HTTP request encoded in www-form-urlencoded
+ *
+ * @access public
+ * @param string $url
+ * @param array $data
+ * @param array $headers
+ * @return string
+ */
+ public function postForm($url, array $data, array $headers = array())
+ {
+ return $this->doRequest(
+ $url,
+ http_build_query($data),
+ array_merge(array('Content-type: application/x-www-form-urlencoded'), $headers)
+ );
+ }
+
+ /**
+ * Make the HTTP request
+ *
+ * @access private
+ * @param string $url
+ * @param array $content
+ * @param array $headers
+ * @return string
+ */
+ private function doRequest($url, $content, array $headers)
+ {
+ if (empty($url)) {
+ return '';
+ }
+
+ $headers = array_merge(array('User-Agent: '.self::HTTP_USER_AGENT, 'Connection: close'), $headers);
+
+ $context = stream_context_create(array(
+ 'http' => array(
+ 'method' => 'POST',
+ 'protocol_version' => 1.1,
+ 'timeout' => self::HTTP_TIMEOUT,
+ 'max_redirects' => self::HTTP_MAX_REDIRECTS,
+ 'header' => implode("\r\n", $headers),
+ 'content' => $content
+ )
+ ));
+
+ $stream = @fopen(trim($url), 'r', false, $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;
+ }
+}
diff --git a/app/Core/Listener.php b/app/Core/Listener.php
deleted file mode 100644
index 9c96cd57..00000000
--- a/app/Core/Listener.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-namespace Core;
-
-/**
- * Event listener interface
- *
- * @package core
- * @author Frederic Guillot
- */
-interface Listener
-{
- /**
- * Execute the listener
- *
- * @access public
- * @param array $data Event data
- * @return boolean
- */
- public function execute(array $data);
-}
diff --git a/app/Core/Loader.php b/app/Core/Loader.php
deleted file mode 100644
index 151081c1..00000000
--- a/app/Core/Loader.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-namespace Core;
-
-/**
- * Loader class
- *
- * @package core
- * @author Frederic Guillot
- */
-class Loader
-{
- /**
- * List of paths
- *
- * @access private
- * @var array
- */
- private $paths = array();
-
- /**
- * Load the missing class
- *
- * @access public
- * @param string $class Class name with namespace
- */
- public function load($class)
- {
- foreach ($this->paths as $path) {
-
- $filename = $path.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php';
-
- if (file_exists($filename)) {
- require $filename;
- break;
- }
- }
- }
-
- /**
- * Register the autoloader
- *
- * @access public
- */
- public function execute()
- {
- spl_autoload_register(array($this, 'load'));
- }
-
- /**
- * Register a new path
- *
- * @access public
- * @param string $path Path
- * @return Core\Loader
- */
- public function setPath($path)
- {
- $this->paths[] = $path;
- return $this;
- }
-}
diff --git a/app/Core/Markdown.php b/app/Core/Markdown.php
new file mode 100644
index 00000000..fa4e8080
--- /dev/null
+++ b/app/Core/Markdown.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Core;
+
+use Parsedown;
+use Helper\Url;
+
+/**
+ * Specific Markdown rules for Kanboard
+ *
+ * @package core
+ * @author norcnorc
+ * @author Frederic Guillot
+ */
+class Markdown extends Parsedown
+{
+ private $link;
+ private $helper;
+
+ public function __construct($link, Url $helper)
+ {
+ $this->link = $link;
+ $this->helper = $helper;
+ $this->InlineTypes['#'][] = 'TaskLink';
+ $this->inlineMarkerList .= '#';
+ }
+
+ protected function inlineTaskLink($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(
+ $this->link['controller'],
+ $this->link['action'],
+ $this->link['params'] + array('task_id' => $matches[1])
+ );
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[0],
+ 'attributes' => array('href' => $url)));
+ }
+ }
+}
diff --git a/app/Core/MemoryCache.php b/app/Core/MemoryCache.php
new file mode 100644
index 00000000..f80a66ef
--- /dev/null
+++ b/app/Core/MemoryCache.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Core;
+
+class MemoryCache extends Cache
+{
+ private $storage = array();
+
+ public function init()
+ {
+ }
+
+ public function set($key, $value)
+ {
+ $this->storage[$key] = $value;
+ }
+
+ public function get($key)
+ {
+ return isset($this->storage[$key]) ? $this->storage[$key] : null;
+ }
+
+ public function flush()
+ {
+ $this->storage = array();
+ }
+
+ public function remove($key)
+ {
+ unset($this->storage[$key]);
+ }
+}
diff --git a/app/Core/Paginator.php b/app/Core/Paginator.php
new file mode 100644
index 00000000..12cc05a1
--- /dev/null
+++ b/app/Core/Paginator.php
@@ -0,0 +1,461 @@
+<?php
+
+namespace Core;
+
+use Pimple\Container;
+use PicoDb\Table;
+
+/**
+ * Paginator helper
+ *
+ * @package core
+ * @author Frederic Guillot
+ */
+class Paginator
+{
+ /**
+ * Container instance
+ *
+ * @access private
+ * @var \Pimple\Container
+ */
+ private $container;
+
+ /**
+ * Total number of items
+ *
+ * @access private
+ * @var integer
+ */
+ private $total = 0;
+
+ /**
+ * Page number
+ *
+ * @access private
+ * @var integer
+ */
+ private $page = 1;
+
+ /**
+ * Offset
+ *
+ * @access private
+ * @var integer
+ */
+ private $offset = 0;
+
+ /**
+ * Limit
+ *
+ * @access private
+ * @var integer
+ */
+ private $limit = 0;
+
+ /**
+ * Sort by this column
+ *
+ * @access private
+ * @var string
+ */
+ private $order = '';
+
+ /**
+ * Sorting direction
+ *
+ * @access private
+ * @var string
+ */
+ private $direction = 'ASC';
+
+ /**
+ * Slice of items
+ *
+ * @access private
+ * @var array
+ */
+ private $items = array();
+
+ /**
+ * PicoDb Table instance
+ *
+ * @access private
+ * @var \Picodb\Table
+ */
+ private $query = null;
+
+ /**
+ * Controller name
+ *
+ * @access private
+ * @var string
+ */
+ private $controller = '';
+
+ /**
+ * Action name
+ *
+ * @access private
+ * @var string
+ */
+ private $action = '';
+
+ /**
+ * Url params
+ *
+ * @access private
+ * @var array
+ */
+ private $params = array();
+
+ /**
+ * Constructor
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ */
+ public function __construct(Container $container)
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * Set a PicoDb query
+ *
+ * @access public
+ * @param \PicoDb\Table
+ * @return Paginator
+ */
+ public function setQuery(Table $query)
+ {
+ $this->query = $query;
+ $this->total = $this->query->count();
+ return $this;
+ }
+
+ /**
+ * Execute a PicoDb query
+ *
+ * @access public
+ * @return array
+ */
+ public function executeQuery()
+ {
+ if ($this->query !== null) {
+ return $this->query
+ ->offset($this->offset)
+ ->limit($this->limit)
+ ->orderBy($this->order, $this->direction)
+ ->findAll();
+ }
+
+ return array();
+ }
+
+ /**
+ * Set url parameters
+ *
+ * @access public
+ * @param string $controller
+ * @param string $action
+ * @param array $params
+ * @return Paginator
+ */
+ public function setUrl($controller, $action, array $params = array())
+ {
+ $this->controller = $controller;
+ $this->action = $action;
+ $this->params = $params;
+ return $this;
+ }
+
+ /**
+ * Add manually items
+ *
+ * @access public
+ * @param array $items
+ * @return Paginator
+ */
+ public function setCollection(array $items)
+ {
+ $this->items = $items;
+ return $this;
+ }
+
+ /**
+ * Return the items
+ *
+ * @access public
+ * @return array
+ */
+ public function getCollection()
+ {
+ return $this->items ?: $this->executeQuery();
+ }
+
+ /**
+ * Set the total number of items
+ *
+ * @access public
+ * @param integer $total
+ * @return Paginator
+ */
+ public function setTotal($total)
+ {
+ $this->total = $total;
+ return $this;
+ }
+
+ /**
+ * Get the total number of items
+ *
+ * @access public
+ * @return integer
+ */
+ public function getTotal()
+ {
+ return $this->total;
+ }
+
+ /**
+ * Set the default page number
+ *
+ * @access public
+ * @param integer $page
+ * @return Paginator
+ */
+ public function setPage($page)
+ {
+ $this->page = $page;
+ return $this;
+ }
+
+ /**
+ * Set the default column order
+ *
+ * @access public
+ * @param string $order
+ * @return Paginator
+ */
+ public function setOrder($order)
+ {
+ $this->order = $order;
+ return $this;
+ }
+
+ /**
+ * Set the default sorting direction
+ *
+ * @access public
+ * @param string $direction
+ * @return Paginator
+ */
+ public function setDirection($direction)
+ {
+ $this->direction = $direction;
+ return $this;
+ }
+
+ /**
+ * Set the maximum number of items per page
+ *
+ * @access public
+ * @param integer $limit
+ * @return Paginator
+ */
+ public function setMax($limit)
+ {
+ $this->limit = $limit;
+ return $this;
+ }
+
+ /**
+ * Return true if the collection is empty
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isEmpty()
+ {
+ return $this->total === 0;
+ }
+
+ /**
+ * Execute the offset calculation only if the $condition is true
+ *
+ * @access public
+ * @param boolean $condition
+ * @return Paginator
+ */
+ public function calculateOnlyIf($condition)
+ {
+ if ($condition) {
+ $this->calculate();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Calculate the offset value accoring to url params and the page number
+ *
+ * @access public
+ * @return Paginator
+ */
+ public function calculate()
+ {
+ $this->page = $this->container['request']->getIntegerParam('page', 1);
+ $this->direction = $this->container['request']->getStringParam('direction', $this->direction);
+ $this->order = $this->container['request']->getStringParam('order', $this->order);
+
+ if ($this->page < 1) {
+ $this->page = 1;
+ }
+
+ $this->offset = ($this->page - 1) * $this->limit;
+
+ return $this;
+ }
+
+ /**
+ * Get url params for link generation
+ *
+ * @access public
+ * @param integer $page
+ * @param string $order
+ * @param string $direction
+ * @return string
+ */
+ public function getUrlParams($page, $order, $direction)
+ {
+ $params = array(
+ 'page' => $page,
+ 'order' => $order,
+ 'direction' => $direction,
+ );
+
+ return array_merge($this->params, $params);
+ }
+
+ /**
+ * Generate the previous link
+ *
+ * @access public
+ * @return string
+ */
+ public function generatePreviousLink()
+ {
+ $html = '<span class="pagination-previous">';
+
+ if ($this->offset > 0) {
+ $html .= $this->container['helper']->url->link(
+ '&larr; '.t('Previous'),
+ $this->controller,
+ $this->action,
+ $this->getUrlParams($this->page - 1, $this->order, $this->direction)
+ );
+ }
+ else {
+ $html .= '&larr; '.t('Previous');
+ }
+
+ $html .= '</span>';
+
+ return $html;
+ }
+
+ /**
+ * Generate the next link
+ *
+ * @access public
+ * @return string
+ */
+ public function generateNextLink()
+ {
+ $html = '<span class="pagination-next">';
+
+ if (($this->total - $this->offset) > $this->limit) {
+ $html .= $this->container['helper']->url->link(
+ t('Next').' &rarr;',
+ $this->controller,
+ $this->action,
+ $this->getUrlParams($this->page + 1, $this->order, $this->direction)
+ );
+ }
+ else {
+ $html .= t('Next').' &rarr;';
+ }
+
+ $html .= '</span>';
+
+ return $html;
+ }
+
+ /**
+ * Return true if there is no pagination to show
+ *
+ * @access public
+ * @return boolean
+ */
+ public function hasNothingtoShow()
+ {
+ return $this->offset === 0 && ($this->total - $this->offset) <= $this->limit;
+ }
+
+ /**
+ * Generation pagination links
+ *
+ * @access public
+ * @return string
+ */
+ public function toHtml()
+ {
+ $html = '';
+
+ if (! $this->hasNothingtoShow()) {
+ $html .= '<div class="pagination">';
+ $html .= $this->generatePreviousLink();
+ $html .= $this->generateNextLink();
+ $html .= '</div>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Magic method to output pagination links
+ *
+ * @access public
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->toHtml();
+ }
+
+ /**
+ * Column sorting
+ *
+ * @param string $label Column title
+ * @param string $column SQL column name
+ * @return string
+ */
+ public function order($label, $column)
+ {
+ $prefix = '';
+ $direction = 'ASC';
+
+ if ($this->order === $column) {
+ $prefix = $this->direction === 'DESC' ? '&#9660; ' : '&#9650; ';
+ $direction = $this->direction === 'DESC' ? 'ASC' : 'DESC';
+ }
+
+ return $prefix.$this->container['helper']->url->link(
+ $label,
+ $this->controller,
+ $this->action,
+ $this->getUrlParams($this->page, $column, $direction)
+ );
+ }
+}
diff --git a/app/Core/Registry.php b/app/Core/Registry.php
deleted file mode 100644
index d8b9063e..00000000
--- a/app/Core/Registry.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-namespace Core;
-
-use RuntimeException;
-
-/**
- * The registry class is a dependency injection container
- *
- * @property mixed db
- * @property mixed event
- * @package core
- * @author Frederic Guillot
- */
-class Registry
-{
- /**
- * Contains all dependencies
- *
- * @access private
- * @var array
- */
- private $container = array();
-
- /**
- * Contains all instances
- *
- * @access private
- * @var array
- */
- private $instances = array();
-
- /**
- * Set a dependency
- *
- * @access public
- * @param string $name Unique identifier for the service/parameter
- * @param mixed $value The value of the parameter or a closure to define an object
- */
- public function __set($name, $value)
- {
- $this->container[$name] = $value;
- }
-
- /**
- * Get a dependency
- *
- * @access public
- * @param string $name Unique identifier for the service/parameter
- * @return mixed The value of the parameter or an object
- * @throws RuntimeException If the identifier is not found
- */
- public function __get($name)
- {
- if (isset($this->container[$name])) {
-
- if (is_callable($this->container[$name])) {
- return $this->container[$name]();
- }
- else {
- return $this->container[$name];
- }
- }
-
- throw new \RuntimeException('Identifier not found in the registry: '.$name);
- }
-
- /**
- * Return a shared instance of a dependency
- *
- * @access public
- * @param string $name Unique identifier for the service/parameter
- * @return mixed Same object instance of the dependency
- */
- public function shared($name)
- {
- if (! isset($this->instances[$name])) {
- $this->instances[$name] = $this->$name;
- }
-
- return $this->instances[$name];
- }
-}
diff --git a/app/Core/Request.php b/app/Core/Request.php
index a4c426f0..c7ca3184 100644
--- a/app/Core/Request.php
+++ b/app/Core/Request.php
@@ -76,6 +76,17 @@ class Request
}
/**
+ * Get the Json request body
+ *
+ * @access public
+ * @return array
+ */
+ public function getJson()
+ {
+ return json_decode($this->getBody(), true);
+ }
+
+ /**
* Get the content of an uploaded file
*
* @access public
@@ -114,6 +125,20 @@ class Request
}
/**
+ * 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
diff --git a/app/Core/Response.php b/app/Core/Response.php
index 347cdde7..d42a8f1e 100644
--- a/app/Core/Response.php
+++ b/app/Core/Response.php
@@ -168,6 +168,23 @@ class Response
}
/**
+ * Send a css response
+ *
+ * @access public
+ * @param string $data Raw data
+ * @param integer $status_code HTTP status code
+ */
+ public function css($data, $status_code = 200)
+ {
+ $this->status($status_code);
+
+ header('Content-Type: text/css; charset=utf-8');
+ echo $data;
+
+ exit;
+ }
+
+ /**
* Send a binary response
*
* @access public
@@ -195,24 +212,7 @@ class Response
$policies['default-src'] = "'self'";
$values = '';
- foreach ($policies as $policy => $hosts) {
-
- if (is_array($hosts)) {
-
- $acl = '';
-
- foreach ($hosts as &$host) {
-
- if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) {
- $acl .= $host.' ';
- }
- }
- }
- else {
-
- $acl = $hosts;
- }
-
+ foreach ($policies as $policy => $acl) {
$values .= $policy.' '.trim($acl).'; ';
}
@@ -246,7 +246,7 @@ class Response
*/
public function hsts()
{
- if (Tool::isHTTPS()) {
+ if (Request::isHTTPS()) {
header('Strict-Transport-Security: max-age=31536000');
}
}
diff --git a/app/Core/Router.php b/app/Core/Router.php
index c9af6e2c..36c11a0a 100644
--- a/app/Core/Router.php
+++ b/app/Core/Router.php
@@ -2,6 +2,8 @@
namespace Core;
+use Pimple\Container;
+
/**
* Router class
*
@@ -27,24 +29,24 @@ class Router
private $action = '';
/**
- * Registry instance
+ * Container instance
*
* @access private
- * @var \Core\Registry
+ * @var \Pimple\Container
*/
- private $registry;
+ private $container;
/**
* Constructor
*
* @access public
- * @param Registry $registry Registry instance
- * @param string $controller Controller name
- * @param string $action Action name
+ * @param \Pimple\Container $container Container instance
+ * @param string $controller Controller name
+ * @param string $action Action name
*/
- public function __construct(Registry $registry, $controller = '', $action = '')
+ public function __construct(Container $container, $controller = '', $action = '')
{
- $this->registry = $registry;
+ $this->container = $container;
$this->controller = empty($_GET['controller']) ? $controller : $_GET['controller'];
$this->action = empty($_GET['action']) ? $action : $_GET['action'];
}
@@ -81,11 +83,7 @@ class Router
return false;
}
- $instance = new $class($this->registry);
- $instance->request = new Request;
- $instance->response = new Response;
- $instance->session = new Session;
- $instance->template = new Template;
+ $instance = new $class($this->container);
$instance->beforeAction($this->controller, $this->action);
$instance->$method();
diff --git a/app/Core/Session.php b/app/Core/Session.php
index 6028f0b9..c35014cd 100644
--- a/app/Core/Session.php
+++ b/app/Core/Session.php
@@ -2,13 +2,15 @@
namespace Core;
+use ArrayAccess;
+
/**
* Session class
*
* @package core
* @author Frederic Guillot
*/
-class Session
+class Session implements ArrayAccess
{
/**
* Sesion lifetime
@@ -36,32 +38,32 @@ class Session
*
* @access public
* @param string $base_path Cookie path
- * @param string $save_path Custom session save path
*/
- public function open($base_path = '/', $save_path = '')
+ public function open($base_path = '/')
{
- if ($save_path !== '') {
- session_save_path($save_path);
- }
-
+ $base_path = str_replace('\\', '/', $base_path);
+
// HttpOnly and secure flags for session cookie
session_set_cookie_params(
self::SESSION_LIFETIME,
$base_path ?: '/',
null,
- Tool::isHTTPS(),
+ Request::isHTTPS(),
true
);
// Avoid session id in the URL
ini_set('session.use_only_cookies', '1');
+ // Enable strict mode
+ 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 session was autostarted with session.auto_start = 1 in php.ini destroy it, otherwise we cannot login
+ // If the session was autostarted with session.auto_start = 1 in php.ini destroy it
if (isset($_SESSION)) {
session_destroy();
}
@@ -90,19 +92,17 @@ class Session
$_SESSION = array();
// Destroy the session cookie
- if (ini_get('session.use_cookies')) {
- $params = session_get_cookie_params();
-
- setcookie(
- session_name(),
- '',
- time() - 42000,
- $params['path'],
- $params['domain'],
- $params['secure'],
- $params['httponly']
- );
- }
+ $params = session_get_cookie_params();
+
+ setcookie(
+ session_name(),
+ '',
+ time() - 42000,
+ $params['path'],
+ $params['domain'],
+ $params['secure'],
+ $params['httponly']
+ );
// Destroy session data
session_destroy();
@@ -129,4 +129,24 @@ class Session
{
$_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/Template.php b/app/Core/Template.php
index f21e8a6d..9688c2a5 100644
--- a/app/Core/Template.php
+++ b/app/Core/Template.php
@@ -10,28 +10,28 @@ use LogicException;
* @package core
* @author Frederic Guillot
*/
-class Template
+class Template extends Helper
{
/**
* Template path
*
* @var string
*/
- const PATH = 'app/Templates/';
+ const PATH = 'app/Template/';
/**
- * Load a template
+ * Render a template
*
* Example:
*
- * $template->load('template_name', ['bla' => 'value']);
+ * $template->render('template_name', ['bla' => 'value']);
*
* @access public
* @params string $__template_name Template name
* @params array $__template_args Key/Value map of template variables
* @return string
*/
- public function load($__template_name, array $__template_args = array())
+ public function render($__template_name, array $__template_args = array())
{
$__template_file = self::PATH.$__template_name.'.php';
@@ -57,9 +57,9 @@ class Template
*/
public function layout($template_name, array $template_args = array(), $layout_name = 'layout')
{
- return $this->load(
+ return $this->render(
$layout_name,
- $template_args + array('content_for_layout' => $this->load($template_name, $template_args))
+ $template_args + array('content_for_layout' => $this->render($template_name, $template_args))
);
}
}
diff --git a/app/Core/Tool.php b/app/Core/Tool.php
index e54a0d3b..84e42ba8 100644
--- a/app/Core/Tool.php
+++ b/app/Core/Tool.php
@@ -33,35 +33,22 @@ class Tool
}
/**
- * Load and register a model
+ * Get the mailbox hash from an email address
*
* @static
* @access public
- * @param Core\Registry $registry DPI container
- * @param string $name Model name
- * @return mixed
+ * @param string $email
+ * @return string
*/
- public static function loadModel(Registry $registry, $name)
+ public static function getMailboxHash($email)
{
- if (! isset($registry->$name)) {
- $class = '\Model\\'.ucfirst($name);
- $registry->$name = new $class($registry);
+ if (! strpos($email, '@') || ! strpos($email, '+')) {
+ return '';
}
- return $registry->shared($name);
- }
+ list($local_part,) = explode('@', $email);
+ list(,$identifier) = explode('+', $local_part);
- /**
- * 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 $identifier;
}
}
diff --git a/app/Core/Translator.php b/app/Core/Translator.php
index 43e934a9..e3d19692 100644
--- a/app/Core/Translator.php
+++ b/app/Core/Translator.php
@@ -11,14 +11,14 @@ namespace Core;
class Translator
{
/**
- * Locales path
+ * Locale path
*
* @var string
*/
- const PATH = 'app/Locales/';
+ const PATH = 'app/Locale/';
/**
- * Locales
+ * Locale
*
* @static
* @access private
@@ -27,6 +27,31 @@ class Translator
private static $locales = array();
/**
+ * Instance
+ *
+ * @static
+ * @access private
+ * @var Translator
+ */
+ private static $instance = null;
+
+ /**
+ * Get instance
+ *
+ * @static
+ * @access public
+ * @return Translator
+ */
+ public static function getInstance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new self;
+ }
+
+ return self::$instance;
+ }
+
+ /**
* Get a translation
*
* $translator->translate('I have %d kids', 5);
@@ -181,5 +206,8 @@ class Translator
if (file_exists($filename)) {
self::$locales = require $filename;
}
+ else {
+ self::$locales = array();
+ }
}
}
diff --git a/app/Event/AuthEvent.php b/app/Event/AuthEvent.php
new file mode 100644
index 00000000..ec1bec99
--- /dev/null
+++ b/app/Event/AuthEvent.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace 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/Base.php b/app/Event/Base.php
deleted file mode 100644
index 745871a5..00000000
--- a/app/Event/Base.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-namespace Event;
-
-use Core\Listener;
-use Core\Registry;
-use Core\Tool;
-
-/**
- * Base Listener
- *
- * @package event
- * @author Frederic Guillot
- *
- * @property \Model\Comment $comment
- * @property \Model\Project $project
- * @property \Model\ProjectActivity $projectActivity
- * @property \Model\SubTask $subTask
- * @property \Model\Task $task
- * @property \Model\TaskFinder $taskFinder
- */
-abstract class Base implements Listener
-{
- /**
- * Registry instance
- *
- * @access protected
- * @var \Core\Registry
- */
- protected $registry;
-
- /**
- * Constructor
- *
- * @access public
- * @param \Core\Registry $registry Regsitry instance
- */
- public function __construct(Registry $registry)
- {
- $this->registry = $registry;
- }
-
- /**
- * Return class information
- *
- * @access public
- * @return string
- */
- public function __toString()
- {
- return get_called_class();
- }
-
- /**
- * Load automatically models
- *
- * @access public
- * @param string $name Model name
- * @return mixed
- */
- public function __get($name)
- {
- return Tool::loadModel($this->registry, $name);
- }
-
- /**
- * Get event namespace
- *
- * Event = task.close | Namespace = task
- *
- * @access public
- * @return string
- */
- public function getEventNamespace()
- {
- $event_name = $this->registry->event->getLastTriggeredEvent();
- return substr($event_name, 0, strpos($event_name, '.'));
- }
-}
diff --git a/app/Event/CommentEvent.php b/app/Event/CommentEvent.php
new file mode 100644
index 00000000..75a132d7
--- /dev/null
+++ b/app/Event/CommentEvent.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Event;
+
+class CommentEvent extends GenericEvent
+{
+}
diff --git a/app/Event/FileEvent.php b/app/Event/FileEvent.php
new file mode 100644
index 00000000..81bd83c9
--- /dev/null
+++ b/app/Event/FileEvent.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Event;
+
+class FileEvent extends GenericEvent
+{
+}
diff --git a/app/Event/GenericEvent.php b/app/Event/GenericEvent.php
new file mode 100644
index 00000000..b29d8f32
--- /dev/null
+++ b/app/Event/GenericEvent.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Event;
+
+use ArrayAccess;
+use Symfony\Component\EventDispatcher\Event as BaseEvent;
+
+class GenericEvent extends BaseEvent implements ArrayAccess
+{
+ private $container = array();
+
+ public function __construct(array $values = array())
+ {
+ $this->container = $values;
+ }
+
+ public function getAll()
+ {
+ return $this->container;
+ }
+
+ public function offsetSet($offset, $value)
+ {
+ if (is_null($offset)) {
+ $this->container[] = $value;
+ } else {
+ $this->container[$offset] = $value;
+ }
+ }
+
+ public function offsetExists($offset)
+ {
+ return isset($this->container[$offset]);
+ }
+
+ public function offsetUnset($offset)
+ {
+ unset($this->container[$offset]);
+ }
+
+ public function offsetGet($offset)
+ {
+ return isset($this->container[$offset]) ? $this->container[$offset] : null;
+ }
+}
diff --git a/app/Event/NotificationListener.php b/app/Event/NotificationListener.php
deleted file mode 100644
index 3c049327..00000000
--- a/app/Event/NotificationListener.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-namespace Event;
-
-/**
- * Notification listener
- *
- * @package event
- * @author Frederic Guillot
- */
-class NotificationListener extends Base
-{
- /**
- * Template name
- *
- * @accesss private
- * @var string
- */
- private $template = '';
-
- /**
- * Set template name
- *
- * @access public
- * @param string $template Template name
- */
- public function setTemplate($template)
- {
- $this->template = $template;
- }
-
- /**
- * 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 execute(array $data)
- {
- $values = $this->getTemplateData($data);
- $users = $this->notification->getUsersList($values['task']['project_id']);
-
- if ($users) {
- $this->notification->sendEmails($this->template, $users, $values);
- return true;
- }
-
- return false;
- }
-
- /**
- * Fetch data for the mail template
- *
- * @access public
- * @param array $data Event data
- * @return array
- */
- public function getTemplateData(array $data)
- {
- $values = array();
-
- switch ($this->getEventNamespace()) {
- case 'task':
- $values['task'] = $this->taskFinder->getDetails($data['task_id']);
- break;
- case 'subtask':
- $values['subtask'] = $this->subtask->getById($data['id'], true);
- $values['task'] = $this->taskFinder->getDetails($data['task_id']);
- break;
- case 'file':
- $values['file'] = $data;
- $values['task'] = $this->taskFinder->getDetails($data['task_id']);
- break;
- case 'comment':
- $values['comment'] = $this->comment->getById($data['id']);
- $values['task'] = $this->taskFinder->getDetails($values['comment']['task_id']);
- break;
- }
-
- return $values;
- }
-}
diff --git a/app/Event/ProjectActivityListener.php b/app/Event/ProjectActivityListener.php
deleted file mode 100644
index 8958bd2b..00000000
--- a/app/Event/ProjectActivityListener.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-namespace Event;
-
-/**
- * Project activity listener
- *
- * @package event
- * @author Frederic Guillot
- */
-class ProjectActivityListener extends Base
-{
- /**
- * 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 execute(array $data)
- {
- if (isset($data['task_id'])) {
-
- $values = $this->getValues($data);
-
- return $this->projectActivity->createEvent(
- $values['task']['project_id'],
- $values['task']['id'],
- $this->acl->getUserId(),
- $this->registry->event->getLastTriggeredEvent(),
- $values
- );
- }
-
- return false;
- }
-
- /**
- * Get event activity data
- *
- * @access private
- * @param array $data Event data dictionary
- * @return array
- */
- private function getValues(array $data)
- {
- $values = array();
- $values['task'] = $this->taskFinder->getDetails($data['task_id']);
-
- switch ($this->getEventNamespace()) {
- case 'subtask':
- $values['subtask'] = $this->subTask->getById($data['id'], true);
- break;
- case 'comment':
- $values['comment'] = $this->comment->getById($data['id']);
- break;
- }
-
- return $values;
- }
-}
diff --git a/app/Event/ProjectModificationDateListener.php b/app/Event/ProjectModificationDateListener.php
deleted file mode 100644
index abc176b0..00000000
--- a/app/Event/ProjectModificationDateListener.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-namespace Event;
-
-/**
- * Project modification date listener
- *
- * Update the "last_modified" field for a project
- *
- * @package event
- * @author Frederic Guillot
- */
-class ProjectModificationDateListener extends Base
-{
- /**
- * 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 execute(array $data)
- {
- if (isset($data['project_id'])) {
- return $this->project->updateModificationDate($data['project_id']);
- }
-
- return false;
- }
-}
diff --git a/app/Event/SubtaskEvent.php b/app/Event/SubtaskEvent.php
new file mode 100644
index 00000000..229db860
--- /dev/null
+++ b/app/Event/SubtaskEvent.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Event;
+
+class SubtaskEvent extends GenericEvent
+{
+}
diff --git a/app/Event/TaskEvent.php b/app/Event/TaskEvent.php
new file mode 100644
index 00000000..e2fb30fe
--- /dev/null
+++ b/app/Event/TaskEvent.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Event;
+
+class TaskEvent extends GenericEvent
+{
+}
diff --git a/app/Event/WebhookListener.php b/app/Event/WebhookListener.php
deleted file mode 100644
index f7e23e07..00000000
--- a/app/Event/WebhookListener.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-namespace Event;
-
-/**
- * Webhook task events
- *
- * @package event
- * @author Frederic Guillot
- */
-class WebhookListener extends Base
-{
- /**
- * Url to call
- *
- * @access private
- * @var string
- */
- private $url = '';
-
- /**
- * Set webhook url
- *
- * @access public
- * @param string $url URL to call
- */
- public function setUrl($url)
- {
- $this->url = $url;
- }
-
- /**
- * 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 execute(array $data)
- {
- $this->webhook->notify($this->url, $data);
- return true;
- }
-}
diff --git a/app/Helper/App.php b/app/Helper/App.php
new file mode 100644
index 00000000..8f591143
--- /dev/null
+++ b/app/Helper/App.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Helper;
+
+/**
+ * Application helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class App extends \Core\Base
+{
+ /**
+ * Get javascript language code
+ *
+ * @access public
+ * @return string
+ */
+ public function jsLang()
+ {
+ return $this->config->getJsLanguageCode();
+ }
+
+ /**
+ * Get current timezone
+ *
+ * @access public
+ * @return string
+ */
+ public function getTimezone()
+ {
+ return $this->config->getCurrentTimezone();
+ }
+
+ /**
+ * Get session flash message
+ *
+ * @access public
+ * @return string
+ */
+ 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']);
+ }
+ else if (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_error_message']);
+ }
+
+ return $html;
+ }
+}
diff --git a/app/Helper/Asset.php b/app/Helper/Asset.php
new file mode 100644
index 00000000..fe285081
--- /dev/null
+++ b/app/Helper/Asset.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Helper;
+
+/**
+ * Assets helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Asset extends \Core\Base
+{
+ /**
+ * Add a Javascript asset
+ *
+ * @param string $filename Filename
+ * @return string
+ */
+ public function js($filename)
+ {
+ return '<script type="text/javascript" src="'.$filename.'?'.filemtime($filename).'"></script>';
+ }
+
+ /**
+ * Add a stylesheet asset
+ *
+ * @param string $filename Filename
+ * @param boolean $is_file Add file timestamp
+ * @param string $media Media
+ * @return string
+ */
+ public function css($filename, $is_file = true, $media = 'screen')
+ {
+ return '<link rel="stylesheet" href="'.$filename.($is_file ? '?'.filemtime($filename) : '').'" media="'.$media.'">';
+ }
+
+ /**
+ * Get custom css
+ *
+ * @access public
+ * @return string
+ */
+ public function customCss()
+ {
+ if ($this->config->get('application_stylesheet')) {
+ return '<style>'.$this->config->get('application_stylesheet').'</style>';
+ }
+
+ return '';
+ }
+}
diff --git a/app/Helper/Datetime.php b/app/Helper/Datetime.php
new file mode 100644
index 00000000..3a9c4c48
--- /dev/null
+++ b/app/Helper/Datetime.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Helper;
+
+/**
+ * DateTime helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Datetime extends \Core\Base
+{
+ /**
+ * Get all hours for day
+ *
+ * @access public
+ * @return array
+ */
+ public function getDayHours()
+ {
+ $values = array();
+
+ foreach (range(0, 23) as $hour) {
+ foreach (array(0, 30) as $minute) {
+ $time = sprintf('%02d:%02d', $hour, $minute);
+ $values[$time] = $time;
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Get all days of a week
+ *
+ * @access public
+ * @return array
+ */
+ public function getWeekDays()
+ {
+ $values = array();
+
+ foreach (range(1, 7) as $day) {
+ $values[$day] = $this->getWeekDay($day);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Get the localized day name from the day number
+ *
+ * @access public
+ * @param integer $day Day number
+ * @return string
+ */
+ public function getWeekDay($day)
+ {
+ return dt('%A', strtotime('next Monday +'.($day - 1).' days'));
+ }
+}
diff --git a/app/Helper/File.php b/app/Helper/File.php
new file mode 100644
index 00000000..a35e4283
--- /dev/null
+++ b/app/Helper/File.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Helper;
+
+/**
+ * File helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class File extends \Core\Base
+{
+ /**
+ * Get file icon
+ *
+ * @access public
+ * @param string $filename Filename
+ * @return string Font-Awesome-Icon-Name
+ */
+ public function icon($filename){
+
+ $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+
+ switch ($extension) {
+ case 'jpeg':
+ case 'jpg':
+ case 'png':
+ case 'gif':
+ return 'fa-file-image-o';
+ case 'xls':
+ case 'xlsx':
+ return 'fa-file-excel-o';
+ case 'doc':
+ case 'docx':
+ return 'fa-file-word-o';
+ case 'ppt':
+ case 'pptx':
+ return 'fa-file-powerpoint-o';
+ case 'zip':
+ case 'rar':
+ return 'fa-file-archive-o';
+ case 'mp3':
+ return 'fa-audio-o';
+ case 'avi':
+ return 'fa-video-o';
+ case 'php':
+ case 'html':
+ case 'css':
+ return 'fa-code-o';
+ case 'pdf':
+ return 'fa-file-pdf-o';
+ }
+
+ return 'fa-file-o';
+ }
+}
diff --git a/app/Helper/Form.php b/app/Helper/Form.php
new file mode 100644
index 00000000..83e3b113
--- /dev/null
+++ b/app/Helper/Form.php
@@ -0,0 +1,323 @@
+<?php
+
+namespace Helper;
+
+use Core\Security;
+
+/**
+ * Form helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Form extends \Core\Base
+{
+ /**
+ * Hidden CSRF token field
+ *
+ * @access public
+ * @return string
+ */
+ public function csrf()
+ {
+ return '<input type="hidden" name="csrf_token" value="'.Security::getCSRFToken().'"/>';
+ }
+
+ /**
+ * Display a hidden form field
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $values Form values
+ * @return string
+ */
+ public function hidden($name, array $values = array())
+ {
+ return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.$this->formValue($values, $name).'/>';
+ }
+
+ /**
+ * Display a select field
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $options Options
+ * @param array $values Form values
+ * @param array $errors Form errors
+ * @param string $class CSS class
+ * @return string
+ */
+ public function select($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ $html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '.implode(' ', $attributes).'>';
+
+ foreach ($options as $id => $value) {
+
+ $html .= '<option value="'.$this->helper->e($id).'"';
+
+ if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"';
+ if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"';
+
+ $html .= '>'.$this->helper->e($value).'</option>';
+ }
+
+ $html .= '</select>';
+ $html .= $this->errorList($errors, $name);
+
+ return $html;
+ }
+
+ /**
+ * Display a radio field group
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $options Options
+ * @param array $values Form values
+ * @return string
+ */
+ public function radios($name, array $options, array $values = array())
+ {
+ $html = '';
+
+ foreach ($options as $value => $label) {
+ $html .= $this->radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value);
+ }
+
+ return $html;
+ }
+
+ /**
+ * Display a radio field
+ *
+ * @access public
+ * @param string $name Field name
+ * @param string $label Form label
+ * @param string $value Form value
+ * @param boolean $selected Field selected or not
+ * @param string $class CSS class
+ * @return string
+ */
+ public function radio($name, $label, $value, $selected = false, $class = '')
+ {
+ return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($selected ? 'checked="checked"' : '').'> '.$this->helper->e($label).'</label>';
+ }
+
+ /**
+ * Display a checkbox field
+ *
+ * @access public
+ * @param string $name Field name
+ * @param string $label Form label
+ * @param string $value Form value
+ * @param boolean $checked Field selected or not
+ * @param string $class CSS class
+ * @return string
+ */
+ public function checkbox($name, $label, $value, $checked = false, $class = '')
+ {
+ return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($checked ? 'checked="checked"' : '').'>&nbsp;'.$this->helper->e($label).'</label>';
+ }
+
+ /**
+ * Display a form label
+ *
+ * @access public
+ * @param string $name Field name
+ * @param string $label Form label
+ * @param array $attributes HTML attributes
+ * @return string
+ */
+ public function label($label, $name, array $attributes = array())
+ {
+ return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.$this->helper->e($label).'</label>';
+ }
+
+ /**
+ * Display a textarea
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $values Form values
+ * @param array $errors Form errors
+ * @param array $attributes HTML attributes
+ * @param string $class CSS class
+ * @return string
+ */
+ public function textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ $class .= $this->errorClass($errors, $name);
+
+ $html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" ';
+ $html .= implode(' ', $attributes).'>';
+ $html .= isset($values->$name) ? $this->helper->e($values->$name) : isset($values[$name]) ? $values[$name] : '';
+ $html .= '</textarea>';
+ $html .= $this->errorList($errors, $name);
+
+ return $html;
+ }
+
+ /**
+ * Display a input field
+ *
+ * @access public
+ * @param string $type HMTL input tag type
+ * @param string $name Field name
+ * @param array $values Form values
+ * @param array $errors Form errors
+ * @param array $attributes HTML attributes
+ * @param string $class CSS class
+ * @return string
+ */
+ public function input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ $class .= $this->errorClass($errors, $name);
+
+ $html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.$this->formValue($values, $name).' class="'.$class.'" ';
+ $html .= implode(' ', $attributes).'>';
+
+ if (in_array('required', $attributes)) {
+ $html .= '<span class="form-required">*</span>';
+ }
+
+ $html .= $this->errorList($errors, $name);
+
+ return $html;
+ }
+
+ /**
+ * Display a text field
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $values Form values
+ * @param array $errors Form errors
+ * @param array $attributes HTML attributes
+ * @param string $class CSS class
+ * @return string
+ */
+ public function text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ return $this->input('text', $name, $values, $errors, $attributes, $class);
+ }
+
+ /**
+ * Display a password field
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $values Form values
+ * @param array $errors Form errors
+ * @param array $attributes HTML attributes
+ * @param string $class CSS class
+ * @return string
+ */
+ public function password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ return $this->input('password', $name, $values, $errors, $attributes, $class);
+ }
+
+ /**
+ * Display an email field
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $values Form values
+ * @param array $errors Form errors
+ * @param array $attributes HTML attributes
+ * @param string $class CSS class
+ * @return string
+ */
+ public function email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ return $this->input('email', $name, $values, $errors, $attributes, $class);
+ }
+
+ /**
+ * Display a number field
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $values Form values
+ * @param array $errors Form errors
+ * @param array $attributes HTML attributes
+ * @param string $class CSS class
+ * @return string
+ */
+ public function number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ return $this->input('number', $name, $values, $errors, $attributes, $class);
+ }
+
+ /**
+ * Display a numeric field (allow decimal number)
+ *
+ * @access public
+ * @param string $name Field name
+ * @param array $values Form values
+ * @param array $errors Form errors
+ * @param array $attributes HTML attributes
+ * @param string $class CSS class
+ * @return string
+ */
+ public function numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ return $this->input('text', $name, $values, $errors, $attributes, $class.' form-numeric');
+ }
+
+ /**
+ * Display the form error class
+ *
+ * @access private
+ * @param array $errors Error list
+ * @param string $name Field name
+ * @return string
+ */
+ private function errorClass(array $errors, $name)
+ {
+ return ! isset($errors[$name]) ? '' : ' form-error';
+ }
+
+ /**
+ * Display a list of form errors
+ *
+ * @access private
+ * @param array $errors List of errors
+ * @param string $name Field name
+ * @return string
+ */
+ private function errorList(array $errors, $name)
+ {
+ $html = '';
+
+ if (isset($errors[$name])) {
+
+ $html .= '<ul class="form-errors">';
+
+ foreach ($errors[$name] as $error) {
+ $html .= '<li>'.$this->helper->e($error).'</li>';
+ }
+
+ $html .= '</ul>';
+ }
+
+ return $html;
+ }
+
+ /**
+ * Get an escaped form value
+ *
+ * @access private
+ * @param mixed $values Values
+ * @param string $name Field name
+ * @return string
+ */
+ private function formValue($values, $name)
+ {
+ if (isset($values->$name)) {
+ return 'value="'.$this->helper->e($values->$name).'"';
+ }
+
+ return isset($values[$name]) ? 'value="'.$this->helper->e($values[$name]).'"' : '';
+ }
+}
diff --git a/app/Helper/Subtask.php b/app/Helper/Subtask.php
new file mode 100644
index 00000000..6348ebd1
--- /dev/null
+++ b/app/Helper/Subtask.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Helper;
+
+/**
+ * Subtask helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Subtask extends \Core\Base
+{
+ /**
+ * Get the link to toggle subtask status
+ *
+ * @access public
+ * @param array $subtask
+ * @param string $redirect
+ * @return string
+ */
+ public function toggleStatus(array $subtask, $redirect)
+ {
+ 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'
+ );
+ }
+
+ 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)
+ );
+ }
+}
diff --git a/app/Helper/Task.php b/app/Helper/Task.php
new file mode 100644
index 00000000..b3931cdb
--- /dev/null
+++ b/app/Helper/Task.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Helper;
+
+/**
+ * Task helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Task extends \Core\Base
+{
+ /**
+ * Get the age of an item in quasi human readable format.
+ * It's in this format: <1h , NNh, NNd
+ *
+ * @access public
+ * @param integer $timestamp Unix timestamp of the artifact for which age will be calculated
+ * @param integer $now Compare with this timestamp (Default value is the current unix timestamp)
+ * @return string
+ */
+ public function age($timestamp, $now = null)
+ {
+ if ($now === null) {
+ $now = time();
+ }
+
+ $diff = $now - $timestamp;
+
+ if ($diff < 3600) {
+ return t('<1h');
+ }
+ else if ($diff < 86400) {
+ return t('%dh', $diff / 3600);
+ }
+
+ return t('%dd', ($now - $timestamp) / 86400);
+ }
+
+ public function recurrenceTriggers()
+ {
+ return $this->task->getRecurrenceTriggerList();
+ }
+
+ public function recurrenceTimeframes()
+ {
+ return $this->task->getRecurrenceTimeframeList();
+ }
+
+ public function recurrenceBasedates()
+ {
+ return $this->task->getRecurrenceBasedateList();
+ }
+
+ public function canRemove(array $task)
+ {
+ return $this->taskPermission->canRemoveTask($task);
+ }
+}
diff --git a/app/Helper/Text.php b/app/Helper/Text.php
new file mode 100644
index 00000000..cfb557b1
--- /dev/null
+++ b/app/Helper/Text.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Helper;
+
+use Core\Markdown;
+
+/**
+ * Text helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Text extends \Core\Base
+{
+ /**
+ * Markdown transformation
+ *
+ * @param string $text Markdown content
+ * @param array $link Link parameters for replacement
+ * @return string
+ */
+ public function markdown($text, array $link = array())
+ {
+ $parser = new Markdown($link, $this->helper->url);
+ $parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML);
+ return $parser->text($text);
+ }
+
+ /**
+ * Format a file size
+ *
+ * @param integer $size Size in bytes
+ * @param integer $precision Precision
+ * @return string
+ */
+ public function bytes($size, $precision = 2)
+ {
+ $base = log($size) / log(1024);
+ $suffixes = array('', 'k', 'M', 'G', 'T');
+
+ return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)];
+ }
+
+ /**
+ * Truncate a long text
+ *
+ * @param string $value Text
+ * @param integer $max_length Max Length
+ * @param string $end Text end
+ * @return string
+ */
+ public function truncate($value, $max_length = 85, $end = '[...]')
+ {
+ $length = strlen($value);
+
+ if ($length > $max_length) {
+ return substr($value, 0, $max_length).' '.$end;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Return true if needle is contained in the haystack
+ *
+ * @param string $haystack Haystack
+ * @param string $needle Needle
+ * @return boolean
+ */
+ public function contains($haystack, $needle)
+ {
+ return strpos($haystack, $needle) !== false;
+ }
+
+ /**
+ * Return a value from a dictionary
+ *
+ * @param mixed $id Key
+ * @param array $listing Dictionary
+ * @param string $default_value Value displayed when the key doesn't exists
+ * @return string
+ */
+ public function in($id, array $listing, $default_value = '?')
+ {
+ if (isset($listing[$id])) {
+ return $this->helper->e($listing[$id]);
+ }
+
+ return $default_value;
+ }
+}
diff --git a/app/Helper/Url.php b/app/Helper/Url.php
new file mode 100644
index 00000000..64b2c83f
--- /dev/null
+++ b/app/Helper/Url.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Helper;
+
+use Core\Request;
+use Core\Security;
+
+/**
+ * Url helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class Url extends \Core\Base
+{
+ /**
+ * HTML Link tag
+ *
+ * @access public
+ * @param string $label Link label
+ * @param string $controller Controller name
+ * @param string $action Action name
+ * @param array $params Url parameters
+ * @param boolean $csrf Add a CSRF token
+ * @param string $class CSS class attribute
+ * @param boolean $new_tab Open the link in a new tab
+ * @param string $anchor Link Anchor
+ * @return string
+ */
+ 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>';
+ }
+
+ /**
+ * Hyperlink
+ *
+ * @access public
+ * @param string $controller Controller name
+ * @param string $action Action name
+ * @param array $params Url parameters
+ * @param boolean $csrf Add a CSRF token
+ * @param string $anchor Link Anchor
+ * @return string
+ */
+ public function href($controller, $action, array $params = array(), $csrf = false, $anchor = '')
+ {
+ $values = array(
+ 'controller' => $controller,
+ 'action' => $action,
+ );
+
+ if ($csrf) {
+ $params['csrf_token'] = Security::getCSRFToken();
+ }
+
+ $values += $params;
+
+ return '?'.http_build_query($values, '', '&amp;').(empty($anchor) ? '' : '#'.$anchor);
+ }
+
+ /**
+ * Generate controller/action url
+ *
+ * @access public
+ * @param string $controller Controller name
+ * @param string $action Action name
+ * @param array $params Url parameters
+ * @return string
+ */
+ public function to($controller, $action, array $params = array())
+ {
+ $values = array(
+ 'controller' => $controller,
+ 'action' => $action,
+ );
+
+ $values += $params;
+
+ return '?'.http_build_query($values, '', '&');
+ }
+
+ /**
+ * Get application base url
+ *
+ * @access public
+ * @return string
+ */
+ public function base()
+ {
+ $application_url = $this->config->get('application_url');
+
+ if (! empty($application_url)) {
+ return $application_url;
+ }
+
+ return $this->server();
+ }
+
+ /**
+ * Get current server base url
+ *
+ * @access public
+ * @return string
+ */
+ public function server()
+ {
+ $self = str_replace('\\', '/', dirname($_SERVER['PHP_SELF']));
+
+ $url = Request::isHTTPS() ? 'https://' : 'http://';
+ $url .= $_SERVER['SERVER_NAME'];
+ $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
+ $url .= $self !== '/' ? $self.'/' : '/';
+
+ return $url;
+ }
+}
diff --git a/app/Helper/User.php b/app/Helper/User.php
new file mode 100644
index 00000000..1cad6042
--- /dev/null
+++ b/app/Helper/User.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Helper;
+
+/**
+ * User helpers
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class User extends \Core\Base
+{
+ /**
+ * Get user id
+ *
+ * @access public
+ * @return integer
+ */
+ public function getId()
+ {
+ return $this->userSession->getId();
+ }
+
+ /**
+ * Get user profile
+ *
+ * @access public
+ * @return string
+ */
+ public function getProfileLink()
+ {
+ return $this->helper->url->link(
+ $this->helper->e($this->getFullname()),
+ 'user',
+ 'show',
+ array('user_id' => $this->userSession->getId())
+ );
+ }
+ /**
+ * Check if the given user_id is the connected user
+ *
+ * @param integer $user_id User id
+ * @return boolean
+ */
+ public function isCurrentUser($user_id)
+ {
+ return $this->userSession->getId() == $user_id;
+ }
+
+ /**
+ * Return if the logged user is admin
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isAdmin()
+ {
+ return $this->userSession->isAdmin();
+ }
+
+ /**
+ * Proxy cache helper for acl::isManagerActionAllowed()
+ *
+ * @access public
+ * @param integer $project_id
+ * @return boolean
+ */
+ public function isManager($project_id)
+ {
+ if ($this->userSession->isAdmin()) {
+ return true;
+ }
+
+ return $this->memoryCache->proxy('acl', 'isManagerActionAllowed', $project_id);
+ }
+
+ /**
+ * Return the user full name
+ *
+ * @param array $user User properties
+ * @return string
+ */
+ public function getFullname(array $user = array())
+ {
+ return $this->user->getFullname(empty($user) ? $_SESSION['user'] : $user);
+ }
+
+ /**
+ * Display gravatar image
+ *
+ * @access public
+ * @param string $email
+ * @param string $alt
+ * @return string
+ */
+ public function avatar($email, $alt = '')
+ {
+ if (! empty($email) && $this->config->get('integration_gravatar') == 1) {
+ return '<img class="avatar" src="https://www.gravatar.com/avatar/'.md5(strtolower($email)).'?s=25" alt="'.$this->helper->e($alt).'" title="'.$this->helper->e($alt).'">';
+ }
+
+ return '';
+ }
+}
diff --git a/app/Integration/BitbucketWebhook.php b/app/Integration/BitbucketWebhook.php
new file mode 100644
index 00000000..75fc1c81
--- /dev/null
+++ b/app/Integration/BitbucketWebhook.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Integration;
+
+use Event\TaskEvent;
+use Model\Task;
+
+/**
+ * Bitbucket Webhook
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class BitbucketWebhook extends \Core\Base
+{
+ /**
+ * Events
+ *
+ * @var string
+ */
+ const EVENT_COMMIT = 'bitbucket.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 events
+ *
+ * @access public
+ * @param array $payload Gitlab event
+ * @return boolean
+ */
+ public function parsePayload(array $payload)
+ {
+ if (! empty($payload['commits'])) {
+
+ foreach ($payload['commits'] as $commit) {
+
+ if ($this->handleCommit($commit)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse commit
+ *
+ * @access public
+ * @param array $commit Gitlab commit
+ * @return boolean
+ */
+ public function handleCommit(array $commit)
+ {
+ $task_id = $this->task->getTaskIdFromText($commit['message']);
+
+ if (! $task_id) {
+ return false;
+ }
+
+ $task = $this->taskFinder->getById($task_id);
+
+ if (empty($task)) {
+ return false;
+ }
+
+ if ($task['is_active'] == Task::STATUS_OPEN && $task['project_id'] == $this->project_id) {
+
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_COMMIT,
+ new TaskEvent(array('task_id' => $task_id) + $task)
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/Model/GithubWebhook.php b/app/Integration/GithubWebhook.php
index 6624a782..d95eba78 100644
--- a/app/Model/GithubWebhook.php
+++ b/app/Integration/GithubWebhook.php
@@ -1,14 +1,17 @@
<?php
-namespace Model;
+namespace Integration;
+
+use Event\GenericEvent;
+use Model\Task;
/**
- * Github Webhook model
+ * Github Webhook
*
- * @package model
+ * @package integration
* @author Frederic Guillot
*/
-class GithubWebhook extends Base
+class GithubWebhook extends \Core\Base
{
/**
* Events
@@ -47,18 +50,21 @@ class GithubWebhook extends Base
*
* @access public
* @param string $type Github event type
- * @param string $payload Raw Github event (JSON)
+ * @param array $payload Github event
+ * @return boolean
*/
- public function parsePayload($type, $payload)
+ public function parsePayload($type, array $payload)
{
- $payload = json_decode($payload, true);
-
switch ($type) {
case 'push':
return $this->parsePushEvent($payload);
case 'issues':
return $this->parseIssueEvent($payload);
+ case 'issue_comment':
+ return $this->parseCommentIssueEvent($payload);
}
+
+ return false;
}
/**
@@ -66,6 +72,7 @@ class GithubWebhook extends Base
*
* @access public
* @param array $payload Event data
+ * @return boolean
*/
public function parsePushEvent(array $payload)
{
@@ -79,14 +86,19 @@ class GithubWebhook extends Base
$task = $this->taskFinder->getById($task_id);
- if (! $task) {
+ if (empty($task)) {
continue;
}
- if ($task['is_active'] == Task::STATUS_OPEN) {
- $this->event->trigger(self::EVENT_COMMIT, array('task_id' => $task_id) + $task);
+ if ($task['is_active'] == Task::STATUS_OPEN && $task['project_id'] == $this->project_id) {
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_COMMIT,
+ new GenericEvent(array('task_id' => $task_id) + $task)
+ );
}
}
+
+ return true;
}
/**
@@ -94,32 +106,61 @@ class GithubWebhook extends Base
*
* @access public
* @param array $payload Event data
+ * @return boolean
*/
public function parseIssueEvent(array $payload)
{
switch ($payload['action']) {
case 'opened':
- $this->handleIssueOpened($payload['issue']);
- break;
+ return $this->handleIssueOpened($payload['issue']);
case 'closed':
- $this->handleIssueClosed($payload['issue']);
- break;
+ return $this->handleIssueClosed($payload['issue']);
case 'reopened':
- $this->handleIssueReopened($payload['issue']);
- break;
+ return $this->handleIssueReopened($payload['issue']);
case 'assigned':
- $this->handleIssueAssigned($payload['issue']);
- break;
+ return $this->handleIssueAssigned($payload['issue']);
case 'unassigned':
- $this->handleIssueUnassigned($payload['issue']);
- break;
+ return $this->handleIssueUnassigned($payload['issue']);
case 'labeled':
- $this->handleIssueLabeled($payload['issue'], $payload['label']);
- break;
+ return $this->handleIssueLabeled($payload['issue'], $payload['label']);
case 'unlabeled':
- $this->handleIssueUnlabeled($payload['issue'], $payload['label']);
- break;
+ 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']);
+ $user = $this->user->getByUsername($payload['comment']['user']['login']);
+
+ if (! empty($task) && ! empty($user)) {
+
+ $event = array(
+ 'project_id' => $this->project_id,
+ 'reference' => $payload['comment']['id'],
+ 'comment' => $payload['comment']['body'],
+ 'user_id' => $user['id'],
+ 'task_id' => $task['id'],
+ );
+
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_ISSUE_COMMENT,
+ new GenericEvent($event)
+ );
+
+ return true;
}
+
+ return false;
}
/**
@@ -127,6 +168,7 @@ class GithubWebhook extends Base
*
* @access public
* @param array $issue Issue data
+ * @return boolean
*/
public function handleIssueOpened(array $issue)
{
@@ -137,7 +179,12 @@ class GithubWebhook extends Base
'description' => $issue['body']."\n\n[".t('Github Issue').']('.$issue['html_url'].')',
);
- $this->event->trigger(self::EVENT_ISSUE_OPENED, $event);
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_ISSUE_OPENED,
+ new GenericEvent($event)
+ );
+
+ return true;
}
/**
@@ -145,20 +192,28 @@ class GithubWebhook extends Base
*
* @access public
* @param array $issue Issue data
+ * @return boolean
*/
public function handleIssueClosed(array $issue)
{
- $task = $this->taskFinder->getByReference($issue['number']);
+ $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
- if ($task) {
+ if (! empty($task)) {
$event = array(
'project_id' => $this->project_id,
'task_id' => $task['id'],
'reference' => $issue['number'],
);
- $this->event->trigger(self::EVENT_ISSUE_CLOSED, $event);
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_ISSUE_CLOSED,
+ new GenericEvent($event)
+ );
+
+ return true;
}
+
+ return false;
}
/**
@@ -166,20 +221,28 @@ class GithubWebhook extends Base
*
* @access public
* @param array $issue Issue data
+ * @return boolean
*/
public function handleIssueReopened(array $issue)
{
- $task = $this->taskFinder->getByReference($issue['number']);
+ $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
- if ($task) {
+ if (! empty($task)) {
$event = array(
'project_id' => $this->project_id,
'task_id' => $task['id'],
'reference' => $issue['number'],
);
- $this->event->trigger(self::EVENT_ISSUE_REOPENED, $event);
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_ISSUE_REOPENED,
+ new GenericEvent($event)
+ );
+
+ return true;
}
+
+ return false;
}
/**
@@ -187,13 +250,14 @@ class GithubWebhook extends Base
*
* @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($issue['number']);
+ $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
- if ($user && $task) {
+ if (! empty($user) && ! empty($task)) {
$event = array(
'project_id' => $this->project_id,
@@ -202,8 +266,15 @@ class GithubWebhook extends Base
'reference' => $issue['number'],
);
- $this->event->trigger(self::EVENT_ISSUE_ASSIGNEE_CHANGE, $event);
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_ISSUE_ASSIGNEE_CHANGE,
+ new GenericEvent($event)
+ );
+
+ return true;
}
+
+ return false;
}
/**
@@ -211,12 +282,13 @@ class GithubWebhook extends Base
*
* @access public
* @param array $issue Issue data
+ * @return boolean
*/
public function handleIssueUnassigned(array $issue)
{
- $task = $this->taskFinder->getByReference($issue['number']);
+ $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
- if ($task) {
+ if (! empty($task)) {
$event = array(
'project_id' => $this->project_id,
@@ -225,8 +297,15 @@ class GithubWebhook extends Base
'reference' => $issue['number'],
);
- $this->event->trigger(self::EVENT_ISSUE_ASSIGNEE_CHANGE, $event);
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_ISSUE_ASSIGNEE_CHANGE,
+ new GenericEvent($event)
+ );
+
+ return true;
}
+
+ return false;
}
/**
@@ -235,12 +314,13 @@ class GithubWebhook extends Base
* @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($issue['number']);
+ $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
- if ($task) {
+ if (! empty($task)) {
$event = array(
'project_id' => $this->project_id,
@@ -249,8 +329,15 @@ class GithubWebhook extends Base
'label' => $label['name'],
);
- $this->event->trigger(self::EVENT_ISSUE_LABEL_CHANGE, $event);
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_ISSUE_LABEL_CHANGE,
+ new GenericEvent($event)
+ );
+
+ return true;
}
+
+ return false;
}
/**
@@ -259,12 +346,13 @@ class GithubWebhook extends Base
* @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($issue['number']);
+ $task = $this->taskFinder->getByReference($this->project_id, $issue['number']);
- if ($task) {
+ if (! empty($task)) {
$event = array(
'project_id' => $this->project_id,
@@ -274,7 +362,14 @@ class GithubWebhook extends Base
'category_id' => 0,
);
- $this->event->trigger(self::EVENT_ISSUE_LABEL_CHANGE, $event);
+ $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
new file mode 100644
index 00000000..8a11f5c6
--- /dev/null
+++ b/app/Integration/GitlabWebhook.php
@@ -0,0 +1,213 @@
+<?php
+
+namespace Integration;
+
+use Event\GenericEvent;
+use Event\TaskEvent;
+use Model\Task;
+
+/**
+ * Gitlab Webhook
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class GitlabWebhook extends \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';
+
+ /**
+ * Supported webhook events
+ *
+ * @var string
+ */
+ const TYPE_PUSH = 'push';
+ const TYPE_ISSUE = 'issue';
+
+ /**
+ * 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);
+ }
+
+ return false;
+ }
+
+ /**
+ * Get event type
+ *
+ * @access public
+ * @param array $payload Gitlab event
+ * @return string
+ */
+ public function getType(array $payload)
+ {
+ if (isset($payload['object_kind']) && $payload['object_kind'] === 'issue') {
+ return self::TYPE_ISSUE;
+ }
+
+ if (isset($payload['commits'])) {
+ return self::TYPE_PUSH;
+ }
+
+ 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 (! $task_id) {
+ return false;
+ }
+
+ $task = $this->taskFinder->getById($task_id);
+
+ if (empty($task)) {
+ return false;
+ }
+
+ if ($task['is_active'] == Task::STATUS_OPEN && $task['project_id'] == $this->project_id) {
+
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_COMMIT,
+ new TaskEvent(array('task_id' => $task_id) + $task)
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/app/Integration/HipchatWebhook.php b/app/Integration/HipchatWebhook.php
new file mode 100644
index 00000000..f1be0f34
--- /dev/null
+++ b/app/Integration/HipchatWebhook.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Integration;
+
+/**
+ * Hipchat webhook
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class HipchatWebhook extends \Core\Base
+{
+ /**
+ * Return true if Hipchat is enabled for this project or globally
+ *
+ * @access public
+ * @param integer $project_id
+ * @return boolean
+ */
+ public function isActivated($project_id)
+ {
+ return $this->config->get('integration_hipchat') == 1 || $this->projectIntegration->hasValue($project_id, 'hipchat', 1);
+ }
+
+ /**
+ * Get API parameters
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getParameters($project_id)
+ {
+ if ($this->config->get('integration_hipchat') == 1) {
+ return array(
+ 'api_url' => $this->config->get('integration_hipchat_api_url'),
+ 'room_id' => $this->config->get('integration_hipchat_room_id'),
+ 'room_token' => $this->config->get('integration_hipchat_room_token'),
+ );
+ }
+
+ $options = $this->projectIntegration->getParameters($project_id);
+
+ return array(
+ 'api_url' => $options['hipchat_api_url'],
+ 'room_id' => $options['hipchat_room_id'],
+ 'room_token' => $options['hipchat_room_token'],
+ );
+ }
+
+ /**
+ * Send the notification if activated
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param string $event_name Event name
+ * @param array $event Event data
+ */
+ public function notify($project_id, $task_id, $event_name, array $event)
+ {
+ if ($this->isActivated($project_id)) {
+
+ $params = $this->getParameters($project_id);
+ $project = $this->project->getbyId($project_id);
+
+ $event['event_name'] = $event_name;
+ $event['author'] = $this->user->getFullname($this->session['user']);
+
+ $html = '<img src="http://kanboard.net/assets/img/favicon-32x32.png"/>';
+ $html .= '<strong>'.$project['name'].'</strong>'.(isset($event['task']['title']) ? '<br/>'.$event['task']['title'] : '').'<br/>';
+ $html .= $this->projectActivity->getTitle($event);
+
+ if ($this->config->get('application_url')) {
+ $html .= '<br/><a href="'.$this->config->get('application_url');
+ $html .= $this->helper->url->href('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)).'">';
+ $html .= t('view the task on Kanboard').'</a>';
+ }
+
+ $payload = array(
+ 'message' => $html,
+ 'color' => 'yellow',
+ );
+
+ $url = sprintf(
+ '%s/v2/room/%s/notification?auth_token=%s',
+ $params['api_url'],
+ $params['room_id'],
+ $params['room_token']
+ );
+
+ $this->httpClient->postJson($url, $payload);
+ }
+ }
+}
diff --git a/app/Integration/Jabber.php b/app/Integration/Jabber.php
new file mode 100644
index 00000000..a1191662
--- /dev/null
+++ b/app/Integration/Jabber.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Integration;
+
+use Exception;
+use Fabiang\Xmpp\Options;
+use Fabiang\Xmpp\Client;
+use Fabiang\Xmpp\Protocol\Message;
+use Fabiang\Xmpp\Protocol\Presence;
+
+/**
+ * Jabber
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class Jabber extends \Core\Base
+{
+ /**
+ * Return true if Jabber is enabled for this project or globally
+ *
+ * @access public
+ * @param integer $project_id
+ * @return boolean
+ */
+ public function isActivated($project_id)
+ {
+ return $this->config->get('integration_jabber') == 1 || $this->projectIntegration->hasValue($project_id, 'jabber', 1);
+ }
+
+ /**
+ * Get connection parameters
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getParameters($project_id)
+ {
+ if ($this->config->get('integration_jabber') == 1) {
+ return array(
+ 'server' => $this->config->get('integration_jabber_server'),
+ 'domain' => $this->config->get('integration_jabber_domain'),
+ 'username' => $this->config->get('integration_jabber_username'),
+ 'password' => $this->config->get('integration_jabber_password'),
+ 'nickname' => $this->config->get('integration_jabber_nickname'),
+ 'room' => $this->config->get('integration_jabber_room'),
+ );
+ }
+
+ $options = $this->projectIntegration->getParameters($project_id);
+
+ return array(
+ 'server' => $options['jabber_server'],
+ 'domain' => $options['jabber_domain'],
+ 'username' => $options['jabber_username'],
+ 'password' => $options['jabber_password'],
+ 'nickname' => $options['jabber_nickname'],
+ 'room' => $options['jabber_room'],
+ );
+ }
+
+ /**
+ * Build and send the message
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param string $event_name Event name
+ * @param array $event Event data
+ */
+ public function notify($project_id, $task_id, $event_name, array $event)
+ {
+ if ($this->isActivated($project_id)) {
+
+ $project = $this->project->getbyId($project_id);
+
+ $event['event_name'] = $event_name;
+ $event['author'] = $this->user->getFullname($this->session['user']);
+
+ $payload = '['.$project['name'].'] '.str_replace('&quot;', '"', $this->projectActivity->getTitle($event)).(isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : '');
+
+ if ($this->config->get('application_url')) {
+ $payload .= ' '.$this->config->get('application_url');
+ $payload .= $this->helper->url->to('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id));
+ }
+
+ $this->sendMessage($project_id, $payload);
+ }
+ }
+
+ /**
+ * Send message to the XMPP server
+ *
+ * @access public
+ * @param integer $project_id
+ * @param string $payload
+ */
+ public function sendMessage($project_id, $payload)
+ {
+ try {
+
+ $params = $this->getParameters($project_id);
+
+ $options = new Options($params['server']);
+ $options->setUsername($params['username']);
+ $options->setPassword($params['password']);
+ $options->setTo($params['domain']);
+ $options->setLogger($this->container['logger']);
+
+ $client = new Client($options);
+
+ $channel = new Presence;
+ $channel->setTo($params['room'])->setNickName($params['nickname']);
+ $client->send($channel);
+
+ $message = new Message;
+ $message->setMessage($payload)
+ ->setTo($params['room'])
+ ->setType(Message::TYPE_GROUPCHAT);
+
+ $client->send($message);
+
+ $client->disconnect();
+ }
+ catch (Exception $e) {
+ $this->container['logger']->error('Jabber error: '.$e->getMessage());
+ }
+ }
+}
diff --git a/app/Integration/Mailgun.php b/app/Integration/Mailgun.php
new file mode 100644
index 00000000..1451b211
--- /dev/null
+++ b/app/Integration/Mailgun.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Integration;
+
+use HTML_To_Markdown;
+use Core\Tool;
+
+/**
+ * Mailgun Integration
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class Mailgun extends \Core\Base
+{
+ /**
+ * Send a HTML email
+ *
+ * @access public
+ * @param string $email
+ * @param string $name
+ * @param string $subject
+ * @param string $html
+ * @param string $author
+ */
+ public function sendEmail($email, $name, $subject, $html, $author)
+ {
+ $headers = array(
+ 'Authorization: Basic '.base64_encode('api:'.MAILGUN_API_TOKEN)
+ );
+
+ $payload = array(
+ 'from' => sprintf('%s <%s>', $author, MAIL_FROM),
+ 'to' => sprintf('%s <%s>', $name, $email),
+ 'subject' => $subject,
+ 'html' => $html,
+ );
+
+ $this->httpClient->postForm('https://api.mailgun.net/v3/'.MAILGUN_DOMAIN.'/messages', $payload, $headers);
+ }
+
+ /**
+ * Parse incoming email
+ *
+ * @access public
+ * @param array $payload Incoming email
+ * @return boolean
+ */
+ public function receiveEmail(array $payload)
+ {
+ if (empty($payload['sender']) || empty($payload['subject']) || empty($payload['recipient'])) {
+ return false;
+ }
+
+ // The user must exists in Kanboard
+ $user = $this->user->getByEmail($payload['sender']);
+
+ if (empty($user)) {
+ $this->container['logger']->debug('Mailgun: ignored => user not found');
+ return false;
+ }
+
+ // The project must have a short name
+ $project = $this->project->getByIdentifier(Tool::getMailboxHash($payload['recipient']));
+
+ if (empty($project)) {
+ $this->container['logger']->debug('Mailgun: ignored => project not found');
+ return false;
+ }
+
+ // The user must be member of the project
+ if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
+ $this->container['logger']->debug('Mailgun: ignored => user is not member of the project');
+ return false;
+ }
+
+ // Get the Markdown contents
+ if (! empty($payload['stripped-html'])) {
+ $markdown = new HTML_To_Markdown($payload['stripped-html'], array('strip_tags' => true));
+ $description = $markdown->output();
+ }
+ else if (! empty($payload['stripped-text'])) {
+ $description = $payload['stripped-text'];
+ }
+ else {
+ $description = '';
+ }
+
+ // Finally, we create the task
+ return (bool) $this->taskCreation->create(array(
+ 'project_id' => $project['id'],
+ 'title' => $payload['subject'],
+ 'description' => $description,
+ 'creator_id' => $user['id'],
+ ));
+ }
+}
diff --git a/app/Integration/Postmark.php b/app/Integration/Postmark.php
new file mode 100644
index 00000000..dbb70aee
--- /dev/null
+++ b/app/Integration/Postmark.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Integration;
+
+use HTML_To_Markdown;
+
+/**
+ * Postmark integration
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class Postmark extends \Core\Base
+{
+ /**
+ * Send a HTML email
+ *
+ * @access public
+ * @param string $email
+ * @param string $name
+ * @param string $subject
+ * @param string $html
+ * @param string $author
+ */
+ public function sendEmail($email, $name, $subject, $html, $author)
+ {
+ $headers = array(
+ 'Accept: application/json',
+ 'X-Postmark-Server-Token: '.POSTMARK_API_TOKEN,
+ );
+
+ $payload = array(
+ 'From' => sprintf('%s <%s>', $author, MAIL_FROM),
+ 'To' => sprintf('%s <%s>', $name, $email),
+ 'Subject' => $subject,
+ 'HtmlBody' => $html,
+ );
+
+ $this->httpClient->postJson('https://api.postmarkapp.com/email', $payload, $headers);
+ }
+
+ /**
+ * Parse incoming email
+ *
+ * @access public
+ * @param array $payload Incoming email
+ * @return boolean
+ */
+ public function receiveEmail(array $payload)
+ {
+ if (empty($payload['From']) || empty($payload['Subject']) || empty($payload['MailboxHash'])) {
+ return false;
+ }
+
+ // The user must exists in Kanboard
+ $user = $this->user->getByEmail($payload['From']);
+
+ if (empty($user)) {
+ $this->container['logger']->debug('Postmark: ignored => user not found');
+ return false;
+ }
+
+ // The project must have a short name
+ $project = $this->project->getByIdentifier($payload['MailboxHash']);
+
+ if (empty($project)) {
+ $this->container['logger']->debug('Postmark: ignored => project not found');
+ return false;
+ }
+
+ // The user must be member of the project
+ if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
+ $this->container['logger']->debug('Postmark: ignored => user is not member of the project');
+ return false;
+ }
+
+ // Get the Markdown contents
+ if (! empty($payload['HtmlBody'])) {
+ $markdown = new HTML_To_Markdown($payload['HtmlBody'], array('strip_tags' => true));
+ $description = $markdown->output();
+ }
+ else if (! empty($payload['TextBody'])) {
+ $description = $payload['TextBody'];
+ }
+ else {
+ $description = '';
+ }
+
+ // Finally, we create the task
+ return (bool) $this->taskCreation->create(array(
+ 'project_id' => $project['id'],
+ 'title' => $payload['Subject'],
+ 'description' => $description,
+ 'creator_id' => $user['id'],
+ ));
+ }
+}
diff --git a/app/Integration/SendgridWebhook.php b/app/Integration/SendgridWebhook.php
new file mode 100644
index 00000000..9125f00b
--- /dev/null
+++ b/app/Integration/SendgridWebhook.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Integration;
+
+use HTML_To_Markdown;
+use Core\Tool;
+
+/**
+ * Sendgrid Webhook
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class SendgridWebhook extends \Core\Base
+{
+ /**
+ * Parse incoming email
+ *
+ * @access public
+ * @param array $payload Incoming email
+ * @return boolean
+ */
+ public function parsePayload(array $payload)
+ {
+ if (empty($payload['envelope']) || empty($payload['subject'])) {
+ return false;
+ }
+
+ $envelope = json_decode($payload['envelope'], true);
+ $sender = isset($envelope['to'][0]) ? $envelope['to'][0] : '';
+
+ // The user must exists in Kanboard
+ $user = $this->user->getByEmail($envelope['from']);
+
+ if (empty($user)) {
+ $this->container['logger']->debug('SendgridWebhook: ignored => user not found');
+ return false;
+ }
+
+ // The project must have a short name
+ $project = $this->project->getByIdentifier(Tool::getMailboxHash($sender));
+
+ if (empty($project)) {
+ $this->container['logger']->debug('SendgridWebhook: ignored => project not found');
+ return false;
+ }
+
+ // The user must be member of the project
+ if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
+ $this->container['logger']->debug('SendgridWebhook: ignored => user is not member of the project');
+ return false;
+ }
+
+ // Get the Markdown contents
+ if (! empty($payload['html'])) {
+ $markdown = new HTML_To_Markdown($payload['html'], array('strip_tags' => true));
+ $description = $markdown->output();
+ }
+ else if (! empty($payload['text'])) {
+ $description = $payload['text'];
+ }
+ else {
+ $description = '';
+ }
+
+ // Finally, we create the task
+ return (bool) $this->taskCreation->create(array(
+ 'project_id' => $project['id'],
+ 'title' => $payload['subject'],
+ 'description' => $description,
+ 'creator_id' => $user['id'],
+ ));
+ }
+}
diff --git a/app/Integration/SlackWebhook.php b/app/Integration/SlackWebhook.php
new file mode 100644
index 00000000..975ea21f
--- /dev/null
+++ b/app/Integration/SlackWebhook.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Integration;
+
+/**
+ * Slack Webhook
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class SlackWebhook extends \Core\Base
+{
+ /**
+ * Return true if Slack is enabled for this project or globally
+ *
+ * @access public
+ * @param integer $project_id
+ * @return boolean
+ */
+ public function isActivated($project_id)
+ {
+ return $this->config->get('integration_slack_webhook') == 1 || $this->projectIntegration->hasValue($project_id, 'slack', 1);
+ }
+
+ /**
+ * Get wehbook url
+ *
+ * @access public
+ * @param integer $project_id
+ * @return string
+ */
+ public function getWebhookUrl($project_id)
+ {
+ if ($this->config->get('integration_slack_webhook') == 1) {
+ return $this->config->get('integration_slack_webhook_url');
+ }
+
+ $options = $this->projectIntegration->getParameters($project_id);
+ return $options['slack_webhook_url'];
+ }
+
+ /**
+ * Send message to the incoming Slack webhook
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param string $event_name Event name
+ * @param array $event Event data
+ */
+ public function notify($project_id, $task_id, $event_name, array $event)
+ {
+ if ($this->isActivated($project_id)) {
+
+ $project = $this->project->getbyId($project_id);
+
+ $event['event_name'] = $event_name;
+ $event['author'] = $this->user->getFullname($this->session['user']);
+
+ $payload = array(
+ 'text' => '*['.$project['name'].']* '.str_replace('&quot;', '"', $this->projectActivity->getTitle($event)).(isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : ''),
+ 'username' => 'Kanboard',
+ 'icon_url' => 'http://kanboard.net/assets/img/favicon.png',
+ );
+
+ if ($this->config->get('application_url')) {
+ $payload['text'] .= ' - <'.$this->config->get('application_url');
+ $payload['text'] .= $this->helper->url->href('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id));
+ $payload['text'] .= '|'.t('view the task on Kanboard').'>';
+ }
+
+ $this->httpClient->postJson($this->getWebhookUrl($project_id), $payload);
+ }
+ }
+}
diff --git a/app/Integration/Smtp.php b/app/Integration/Smtp.php
new file mode 100644
index 00000000..ad2f30f8
--- /dev/null
+++ b/app/Integration/Smtp.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Integration;
+
+use Swift_Message;
+use Swift_Mailer;
+use Swift_MailTransport;
+use Swift_SendmailTransport;
+use Swift_SmtpTransport;
+use Swift_TransportException;
+
+/**
+ * Smtp
+ *
+ * @package integration
+ * @author Frederic Guillot
+ */
+class Smtp extends \Core\Base
+{
+ /**
+ * Send a HTML email
+ *
+ * @access public
+ * @param string $email
+ * @param string $name
+ * @param string $subject
+ * @param string $html
+ * @param string $author
+ */
+ public function sendEmail($email, $name, $subject, $html, $author)
+ {
+ try {
+
+ $message = Swift_Message::newInstance()
+ ->setSubject($subject)
+ ->setFrom(array(MAIL_FROM => $author))
+ ->setBody($html, 'text/html')
+ ->setTo(array($email => $name));
+
+ Swift_Mailer::newInstance($this->getTransport())->send($message);
+ }
+ catch (Swift_TransportException $e) {
+ $this->container['logger']->error($e->getMessage());
+ }
+ }
+
+ /**
+ * Get SwiftMailer transport
+ *
+ * @access private
+ * @return \Swift_Transport
+ */
+ private function getTransport()
+ {
+ switch (MAIL_TRANSPORT) {
+ case 'smtp':
+ $transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
+ $transport->setUsername(MAIL_SMTP_USERNAME);
+ $transport->setPassword(MAIL_SMTP_PASSWORD);
+ $transport->setEncryption(MAIL_SMTP_ENCRYPTION);
+ break;
+ case 'sendmail':
+ $transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
+ break;
+ default:
+ $transport = Swift_MailTransport::newInstance();
+ }
+
+ return $transport;
+ }
+}
diff --git a/app/Library/password.php b/app/Library/password.php
new file mode 100644
index 00000000..c6e84cbd
--- /dev/null
+++ b/app/Library/password.php
@@ -0,0 +1,227 @@
+<?php
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * @author Anthony Ferrara <ircmaxell@php.net>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ * @copyright 2012 The Authors
+ */
+
+if (!defined('PASSWORD_BCRYPT')) {
+
+ define('PASSWORD_BCRYPT', 1);
+ define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+
+ if (version_compare(PHP_VERSION, '5.3.7', '<')) {
+
+ define('PASSWORD_PREFIX', '$2a$');
+ }
+ else {
+
+ define('PASSWORD_PREFIX', '$2y$');
+ }
+
+ /**
+ * Hash the password using the specified algorithm
+ *
+ * @param string $password The password to hash
+ * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
+ * @param array $options The options for the algorithm to use
+ *
+ * @return string|false The hashed password, or false on error.
+ */
+ function password_hash($password, $algo, array $options = array()) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
+ return null;
+ }
+ if (!is_string($password)) {
+ trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
+ return null;
+ }
+ if (!is_int($algo)) {
+ trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
+ return null;
+ }
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
+ $cost = 10;
+ if (isset($options['cost'])) {
+ $cost = $options['cost'];
+ if ($cost < 4 || $cost > 31) {
+ trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
+ return null;
+ }
+ }
+ $required_salt_len = 22;
+ $hash_format = sprintf("%s%02d$", PASSWORD_PREFIX, $cost);
+ break;
+ default:
+ trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
+ return null;
+ }
+ if (isset($options['salt'])) {
+ switch (gettype($options['salt'])) {
+ case 'NULL':
+ case 'boolean':
+ case 'integer':
+ case 'double':
+ case 'string':
+ $salt = (string) $options['salt'];
+ break;
+ case 'object':
+ if (method_exists($options['salt'], '__tostring')) {
+ $salt = (string) $options['salt'];
+ break;
+ }
+ case 'array':
+ case 'resource':
+ default:
+ trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
+ return null;
+ }
+ if (strlen($salt) < $required_salt_len) {
+ trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
+ return null;
+ } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
+ $salt = str_replace('+', '.', base64_encode($salt));
+ }
+ } else {
+ $buffer = '';
+ $raw_length = (int) ($required_salt_len * 3 / 4 + 1);
+ $buffer_valid = false;
+ if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
+ $buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
+ $buffer = openssl_random_pseudo_bytes($raw_length);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && is_readable('/dev/urandom')) {
+ $f = fopen('/dev/urandom', 'r');
+ $read = strlen($buffer);
+ while ($read < $raw_length) {
+ $buffer .= fread($f, $raw_length - $read);
+ $read = strlen($buffer);
+ }
+ fclose($f);
+ if ($read >= $raw_length) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid || strlen($buffer) < $raw_length) {
+ $bl = strlen($buffer);
+ for ($i = 0; $i < $raw_length; $i++) {
+ if ($i < $bl) {
+ $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
+ } else {
+ $buffer .= chr(mt_rand(0, 255));
+ }
+ }
+ }
+ $salt = str_replace('+', '.', base64_encode($buffer));
+
+ }
+ $salt = substr($salt, 0, $required_salt_len);
+
+ $hash = $hash_format . $salt;
+
+ $ret = crypt($password, $hash);
+
+ if (!is_string($ret) || strlen($ret) <= 13) {
+ return false;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Get information about the password hash. Returns an array of the information
+ * that was used to generate the password hash.
+ *
+ * array(
+ * 'algo' => 1,
+ * 'algoName' => 'bcrypt',
+ * 'options' => array(
+ * 'cost' => 10,
+ * ),
+ * )
+ *
+ * @param string $hash The password hash to extract info from
+ *
+ * @return array The array of information about the hash.
+ */
+ function password_get_info($hash) {
+ $return = array(
+ 'algo' => 0,
+ 'algoName' => 'unknown',
+ 'options' => array(),
+ );
+ if (substr($hash, 0, 4) == PASSWORD_PREFIX && strlen($hash) == 60) {
+ $return['algo'] = PASSWORD_BCRYPT;
+ $return['algoName'] = 'bcrypt';
+ list($cost) = sscanf($hash, PASSWORD_PREFIX."%d$");
+ $return['options']['cost'] = $cost;
+ }
+ return $return;
+ }
+
+ /**
+ * Determine if the password hash needs to be rehashed according to the options provided
+ *
+ * If the answer is true, after validating the password using password_verify, rehash it.
+ *
+ * @param string $hash The hash to test
+ * @param int $algo The algorithm used for new password hashes
+ * @param array $options The options array passed to password_hash
+ *
+ * @return boolean True if the password needs to be rehashed.
+ */
+ function password_needs_rehash($hash, $algo, array $options = array()) {
+ $info = password_get_info($hash);
+ if ($info['algo'] != $algo) {
+ return true;
+ }
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ $cost = isset($options['cost']) ? $options['cost'] : 10;
+ if ($cost != $info['options']['cost']) {
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Verify a password against a hash using a timing attack resistant approach
+ *
+ * @param string $password The password to verify
+ * @param string $hash The hash to verify against
+ *
+ * @return boolean If the password matches the hash
+ */
+ function password_verify($password, $hash) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
+ return false;
+ }
+ $ret = crypt($password, $hash);
+ if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
+ return false;
+ }
+
+ $status = 0;
+ for ($i = 0; $i < strlen($ret); $i++) {
+ $status |= (ord($ret[$i]) ^ ord($hash[$i]));
+ }
+
+ return $status === 0;
+ }
+}
diff --git a/app/Locales/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 546926af..535d77b8 100644
--- a/app/Locales/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -1,6 +1,8 @@
<?php
return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
'None' => 'Ingen',
'edit' => 'rediger',
'Edit' => 'Rediger',
@@ -182,18 +184,19 @@ return array(
'Change assignee' => 'Ændre ansvarlig',
'Change assignee for the task "%s"' => 'Ændre ansvarlig for opgaven: "%s"',
'Timezone' => 'Tidszone',
- 'Sorry, I didn\'t found this information in my database!' => 'Denne information kunne ikke findes i databasen!',
+ 'Sorry, I didn\'t find this information in my database!' => 'Denne information kunne ikke findes i databasen!',
'Page not found' => 'Siden er ikke fundet',
'Complexity' => 'Kompleksitet',
'limit' => 'Begrænsning',
'Task limit' => 'Opgave begrænsning',
+ // 'Task count' => '',
'This value must be greater than %d' => 'Denne værdi skal være større end %d',
'Edit project access list' => 'Rediger adgangstilladelser for projektet',
'Edit users access' => 'Rediger brugertilladelser',
'Allow this user' => 'Tillad denne bruger',
'Only those users have access to this project:' => 'Kunne disse brugere har adgang til dette projekt:',
'Don\'t forget that administrators have access to everything.' => 'Glem ikke at administratorer har adgang til alt.',
- 'revoke' => 'fjern',
+ 'Revoke' => 'Fjern',
'List of authorized users' => 'Liste over autoriserede brugere',
'User' => 'Bruger',
'Nobody have access to this project.' => 'Ingen har adgang til dette projekt.',
@@ -212,6 +215,7 @@ return array(
'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.',
@@ -385,8 +389,6 @@ return array(
'Creator' => 'Skaber',
'Modification date' => 'Ændringsdato',
'Completion date' => 'Afslutningsdato',
- 'Webhook URL for task creation' => 'Webhook URL for opgave oprettelse',
- 'Webhook URL for task modification' => 'Webhook URL opgave redigering',
'Clone' => 'Kopier',
'Clone Project' => 'Kopier projekt',
'Project cloned successfully.' => 'Projektet er kopieret.',
@@ -406,15 +408,13 @@ return array(
'Comment updated' => 'Kommentar opdateret',
'New comment posted by %s' => 'Ny kommentar af %s',
'List of due tasks for the project "%s"' => 'Udestående opgaver for projektet "%s"',
- '[%s][New attachment] %s (#%d)' => '[%s][Ny vedhæftning] %s (#%d)',
- '[%s][New comment] %s (#%d)' => '[%s][Ny kommentar] %s (#%d)',
- '[%s][Comment updated] %s (#%d)' => '[%s][Kommentar opdateret] %s (#%d)',
- '[%s][New subtask] %s (#%d)' => '[%s][Ny under-opgave] %s (#%d)',
- '[%s][Subtask updated] %s (#%d)' => '[%s][Under-opgave opdateret] %s (#%d)',
- '[%s][New task] %s (#%d)' => '[%s][Ny opgave] %s (#%d)',
- '[%s][Task updated] %s (#%d)' => '[%s][Opgave opdateret] %s (#%d)',
- '[%s][Task closed] %s (#%d)' => '[%s][Opgave lukket] %s (#%d)',
- '[%s][Task opened] %s (#%d)' => '[%s][Opgave åbnet] %s (#%d)',
+ // 'New attachment' => '',
+ // 'New comment' => '',
+ // 'New subtask' => '',
+ // 'Subtask updated' => '',
+ // 'Task updated' => '',
+ // 'Task closed' => '',
+ // 'Task opened' => '',
'[%s][Due tasks]' => 'Udestående opgaver',
'[Kanboard] Notification' => '[Kanboard] Notifikation',
'I want to receive notifications only for those projects:' => 'Jeg vil kun have notifikationer for disse projekter:',
@@ -467,18 +467,18 @@ return array(
'Unable to change the password.' => 'Adgangskoden kunne ikke ændres.',
'Change category for the task "%s"' => 'Skift kategori for opgaven "%s"',
'Change category' => 'Skift kategori',
- '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s opdatert opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s åben opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s flyt opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> til positionen #%d i kolonnen "%s"',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s flyttede opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> til kolonnen "%s"',
- '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s oprettede opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s oprettede en under-opgave for opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s opdaterede en under-opgave for opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
+ '%s updated the task %s' => '%s opdatert opgaven %s',
+ '%s opened the task %s' => '%s åben opgaven %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s flyt opgaven %s til positionen #%d i kolonnen "%s"',
+ '%s moved the task %s to the column "%s"' => '%s flyttede opgaven %s til kolonnen "%s"',
+ '%s created the task %s' => '%s oprettede opgaven %s',
+ // '%s closed the task %s' => '',
+ '%s created a subtask for the task %s' => '%s oprettede en under-opgave for opgaven %s',
+ '%s updated a subtask for the task %s' => '%s opdaterede en under-opgave for opgaven %s',
'Assigned to %s with an estimate of %s/%sh' => 'Tildelt til %s med en estimering på %s/%sh',
'Not assigned, estimate of %sh' => 'Ikke tildelt, estimeret til %sh',
- '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s opdateret en kommentar på opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s har kommenteret opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
+ '%s updated a comment on the task %s' => '%s opdateret en kommentar på opgaven %s',
+ '%s commented the task %s' => '%s har kommenteret opgaven %s',
'%s\'s activity' => '%s\'s aktvitet',
'No activity.' => 'Ingen aktivitet',
'RSS feed' => 'RSS feed',
@@ -497,10 +497,10 @@ return array(
'Default columns for new projects (Comma-separated)' => 'Standard kolonne for nye projekter (kommasepareret)',
'Task assignee change' => 'Opgaven ansvarlig ændring',
'%s change the assignee of the task #%d to %s' => '%s skrift ansvarlig for opgaven #%d til %s',
- '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '%s skift ansvarlig for opgaven <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> til %s',
- '[%s][Column Change] %s (#%d)' => '[%s][Kolonne Skift] %s (#%d)',
- '[%s][Position Change] %s (#%d)' => '[%s][Position Skift] %s (#%d)',
- '[%s][Assignee Change] %s (#%d)' => '[%s][Ansvarlig Skift] %s (#%d)',
+ '%s changed the assignee of the task %s to %s' => '%s skift ansvarlig for opgaven %s til %s',
+ // 'Column Change' => '',
+ // 'Position Change' => '',
+ // 'Assignee Change' => '',
'New password for the user "%s"' => 'Ny adgangskode for brugeren',
'Choose an event' => 'Vælg et event',
'Github commit received' => 'Github commit modtaget',
@@ -551,4 +551,375 @@ return array(
'Confirmation' => 'Bekræftelse',
// 'Allow everybody to access to this project' => '',
// 'Everybody have access to this project.' => '',
+ // 'Webhooks' => '',
+ // 'API' => '',
+ // 'Integration' => '',
+ // 'Github webhooks' => '',
+ // 'Help on Github webhooks' => '',
+ // 'Create a comment from an external provider' => '',
+ // 'Github issue comment created' => '',
+ // 'Configure' => '',
+ // 'Project management' => '',
+ // 'My projects' => '',
+ // 'Columns' => '',
+ // 'Task' => '',
+ // 'Your are not member of any project.' => '',
+ // 'Percentage' => '',
+ // 'Number of tasks' => '',
+ // 'Task distribution' => '',
+ // 'Reportings' => '',
+ // 'Task repartition for "%s"' => '',
+ // 'Analytics' => '',
+ // 'Subtask' => '',
+ // 'My subtasks' => '',
+ // 'User repartition' => '',
+ // 'User repartition for "%s"' => '',
+ // 'Clone this project' => '',
+ // 'Column removed successfully.' => '',
+ // 'Edit Project' => '',
+ // 'Github Issue' => '',
+ // 'Not enough data to show the graph.' => '',
+ // 'Previous' => '',
+ // 'The id must be an integer' => '',
+ // 'The project id must be an integer' => '',
+ // 'The status must be an integer' => '',
+ // 'The subtask id is required' => '',
+ // 'The subtask id must be an integer' => '',
+ // 'The task id is required' => '',
+ // 'The task id must be an integer' => '',
+ // 'The user id must be an integer' => '',
+ // 'This value is required' => '',
+ // 'This value must be numeric' => '',
+ // 'Unable to create this task.' => '',
+ // 'Cumulative flow diagram' => '',
+ // 'Cumulative flow diagram for "%s"' => '',
+ // 'Daily project summary' => '',
+ // 'Daily project summary export' => '',
+ // 'Daily project summary export for "%s"' => '',
+ // 'Exports' => '',
+ // 'This export contains the number of tasks per column grouped per day.' => '',
+ // '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"?' => '',
+ // 'Inactive swimlanes' => '',
+ // 'Set project manager' => '',
+ // 'Set project member' => '',
+ // 'Remove a swimlane' => '',
+ // 'Rename' => '',
+ // 'Show default swimlane' => '',
+ // 'Swimlane modification for the project "%s"' => '',
+ // 'Swimlane not found.' => '',
+ // 'Swimlane removed successfully.' => '',
+ // 'Swimlanes' => '',
+ // 'Swimlane updated successfully.' => '',
+ // 'The default swimlane have been updated successfully.' => '',
+ // 'Unable to 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' => '',
+ // 'Subtasks exportation for "%s"' => '',
+ // 'Task Title' => '',
+ // 'Untitled' => '',
+ // 'Application default' => '',
+ // 'Language:' => '',
+ // 'Timezone:' => '',
+ // 'All columns' => '',
+ // 'Calendar for "%s"' => '',
+ // 'Filter by column' => '',
+ // 'Filter by status' => '',
+ // 'Calendar' => '',
+ // 'Next' => '',
+ // '#%d' => '',
+ // 'Filter by color' => '',
+ // 'Filter by swimlane' => '',
+ // 'All swimlanes' => '',
+ // 'All colors' => '',
+ // 'All status' => '',
+ // 'Add a comment logging moving the task between columns' => '',
+ // 'Moved to column %s' => '',
+ // 'Change description' => '',
+ // 'User dashboard' => '',
+ // 'Allow only one subtask in progress at the same time for a user' => '',
+ // 'Edit column "%s"' => '',
+ // 'Enable time tracking for subtasks' => '',
+ // 'Select the new status of the subtask: "%s"' => '',
+ // 'Subtask timesheet' => '',
+ // 'There is nothing to show.' => '',
+ // 'Time Tracking' => '',
+ // 'You already have one subtask in progress' => '',
+ // 'Which parts of the project do you want to duplicate?' => '',
+ // 'Change dashboard view' => '',
+ // 'Show/hide activities' => '',
+ // 'Show/hide projects' => '',
+ // 'Show/hide subtasks' => '',
+ // 'Show/hide tasks' => '',
+ // 'Disable login form' => '',
+ // 'Show/hide calendar' => '',
+ // 'User calendar' => '',
+ // 'Bitbucket commit received' => '',
+ // 'Bitbucket webhooks' => '',
+ // 'Help on Bitbucket webhooks' => '',
+ // 'Start' => '',
+ // 'End' => '',
+ // 'Task age in days' => '',
+ // 'Days in this column' => '',
+ // '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
+ // 'Filter recently updated' => '',
+ // 'since %B %e, %Y at %k:%M %p' => '',
+ // 'More filters' => '',
+ // 'Compact view' => '',
+ // 'Horizontal scrolling' => '',
+ // 'Compact/wide view' => '',
+ // 'No results match:' => '',
+ // 'Remove hourly rate' => '',
+ // 'Do you really want to remove this hourly rate?' => '',
+ // 'Hourly rates' => '',
+ // 'Hourly rate' => '',
+ // 'Currency' => '',
+ // 'Effective date' => '',
+ // 'Add new rate' => '',
+ // 'Rate removed successfully.' => '',
+ // 'Unable to remove this rate.' => '',
+ // 'Unable to save the hourly rate.' => '',
+ // 'Hourly rate created successfully.' => '',
+ // 'Start time' => '',
+ // 'End time' => '',
+ // 'Comment' => '',
+ // 'All day' => '',
+ // 'Day' => '',
+ // 'Manage timetable' => '',
+ // 'Overtime timetable' => '',
+ // 'Time off timetable' => '',
+ // 'Timetable' => '',
+ // 'Work timetable' => '',
+ // 'Week timetable' => '',
+ // 'Day timetable' => '',
+ // 'From' => '',
+ // 'To' => '',
+ // 'Time slot created successfully.' => '',
+ // 'Unable to save this time slot.' => '',
+ // 'Time slot removed successfully.' => '',
+ // 'Unable to remove this time slot.' => '',
+ // 'Do you really want to remove this time slot?' => '',
+ // 'Remove time slot' => '',
+ // 'Add new time slot' => '',
+ // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
+ // 'Files' => '',
+ // 'Images' => '',
+ // 'Private project' => '',
+ // 'Amount' => '',
+ // 'AUD - Australian Dollar' => '',
+ // 'Budget' => '',
+ // 'Budget line' => '',
+ // 'Budget line removed successfully.' => '',
+ // 'Budget lines' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ // 'Cost' => '',
+ // 'Cost breakdown' => '',
+ // 'Custom Stylesheet' => '',
+ // 'download' => '',
+ // 'Do you really want to remove this budget line?' => '',
+ // 'EUR - Euro' => '',
+ // 'Expenses' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'New budget line' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'Remove a budget line' => '',
+ // 'Remove budget line' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'The budget line have been created successfully.' => '',
+ // 'Unable to create the budget line.' => '',
+ // 'Unable to remove this budget line.' => '',
+ // 'USD - US Dollar' => '',
+ // 'Remaining' => '',
+ // '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' => '',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ // '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' => '',
+ // 'Currency rates are used to calculate project budget.' => '',
+ // 'Reference currency' => '',
+ // 'The currency rate have been added successfully.' => '',
+ // 'Unable to add this currency rate.' => '',
+ // 'Send notifications to a Slack channel' => '',
+ // 'Webhook URL' => '',
+ // 'Help on Slack integration' => '',
+ // '%s remove the assignee of the task %s' => '',
+ // 'Send notifications to Hipchat' => '',
+ // 'API URL' => '',
+ // 'Room API ID or name' => '',
+ // 'Room notification token' => '',
+ // 'Help on Hipchat integration' => '',
+ // '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.' => '',
+ // 'SEK - Swedish Krona' => '',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
);
diff --git a/app/Locales/de_DE/translations.php b/app/Locale/de_DE/translations.php
index f280a802..b56469de 100644
--- a/app/Locales/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -1,6 +1,8 @@
<?php
return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => '.',
'None' => 'Keines',
'edit' => 'Bearbeiten',
'Edit' => 'Bearbeiten',
@@ -35,8 +37,8 @@ return array(
'Users' => 'Benutzer',
'No user' => 'Kein Benutzer',
'Forbidden' => 'Verboten',
- 'Access Forbidden' => 'Zugang verboten',
- 'Only administrators can access to this page.' => 'Nur Administratoren haben Zugang zu dieser Seite.',
+ 'Access Forbidden' => 'Zugriff verboten',
+ 'Only administrators can access to this page.' => 'Nur Administratoren haben Zugriff zu dieser Seite.',
'Edit user' => 'Benutzer bearbeiten',
'Logout' => 'Abmelden',
'Bad username or password' => 'Falscher Benutzername oder Passwort',
@@ -49,7 +51,7 @@ return array(
'No project' => 'Keine Projekte',
'Project' => 'Projekt',
'Status' => 'Status',
- 'Tasks' => 'Aufgabe',
+ 'Tasks' => 'Aufgaben',
'Board' => 'Pinnwand',
'Actions' => 'Aktionen',
'Inactive' => 'Inaktiv',
@@ -57,7 +59,7 @@ return array(
'Column %d' => 'Spalte %d',
'Add this column' => 'Diese Spalte hinzufügen',
'%d tasks on the board' => '%d Aufgaben auf dieser Pinnwand',
- '%d tasks in total' => '%d Aufgaben gesamt',
+ '%d tasks in total' => '%d Aufgaben insgesamt',
'Unable to update this board.' => 'Ändern dieser Pinnwand nicht möglich.',
'Edit board' => 'Pinnwand bearbeiten',
'Disable' => 'Deaktivieren',
@@ -94,7 +96,7 @@ return array(
'User settings' => 'Benutzereinstellungen',
'My default project:' => 'Standardprojekt:',
'Close a task' => 'Aufgabe abschließen',
- 'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich abgeschlossen werden: "%s"?',
+ 'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich geschlossen werden: "%s"?',
'Edit a task' => 'Aufgabe bearbeiten',
'Column' => 'Spalte',
'Color' => 'Farbe',
@@ -108,8 +110,8 @@ return array(
'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 abgeschlossen',
- 'Close this task' => 'Aufgabe abschließen',
+ '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.',
'Add a new task' => 'Neue Aufgabe hinzufügen',
@@ -147,9 +149,9 @@ return array(
'Project disabled successfully.' => 'Projekt erfolgreich deaktiviert.',
'Unable to disable this project.' => 'Deaktivieren des Projekts nicht möglich.',
'Unable to open this task.' => 'Wiedereröffnung der Aufgabe nicht möglich.',
- 'Task opened successfully.' => 'Aufgabe erfolgreich wieder eröffnet.',
+ 'Task opened successfully.' => 'Aufgabe erfolgreich wieder geöffnet.',
'Unable to close this task.' => 'Abschließen der Aufgabe nicht möglich.',
- 'Task closed successfully.' => 'Aufgabe erfolgreich abgeschlossen.',
+ 'Task closed successfully.' => 'Aufgabe erfolgreich geschlossen.',
'Unable to update your task.' => 'Aktualisieren der Aufgabe nicht möglich.',
'Task updated successfully.' => 'Aufgabe erfolgreich aktualisiert.',
'Unable to create your task.' => 'Erstellen der Aufgabe nicht möglich.',
@@ -182,18 +184,19 @@ return array(
'Change assignee' => 'Zuständigkeit ändern',
'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: "%s"',
'Timezone' => 'Zeitzone',
- 'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!',
+ 'Sorry, I didn\'t find this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!',
'Page not found' => 'Seite nicht gefunden',
- // 'Complexity' => '',
+ 'Complexity' => 'Komplexität',
'limit' => 'Limit',
'Task limit' => 'Maximale Anzahl von Aufgaben',
+ 'Task count' => 'Aufgabenanzahl',
'This value must be greater than %d' => 'Dieser Wert muss größer sein als %d',
'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten',
'Edit users access' => 'Benutzerzugriff ändern',
'Allow this user' => 'Diesen Benutzer autorisieren',
- 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugang zum Projekt:',
- 'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugang.',
- 'revoke' => 'entfernen',
+ 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugriff zum Projekt:',
+ '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.',
@@ -201,9 +204,9 @@ return array(
'Comments' => 'Kommentare',
'Post comment' => 'Kommentieren',
'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax',
- 'Leave a comment' => 'Kommentar eingeben...',
+ 'Leave a comment' => 'Kommentar eingeben',
'Comment is required' => 'Ein Kommentar wird benötigt',
- 'Leave a description' => 'Beschreibung eingeben...',
+ 'Leave a description' => 'Beschreibung eingeben',
'Comment added successfully.' => 'Kommentar erfolgreich hinzugefügt.',
'Unable to create your comment.' => 'Hinzufügen eines Kommentars nicht möglich.',
'The description is required' => 'Eine Beschreibung wird benötigt',
@@ -212,8 +215,9 @@ return array(
'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' => '',
'Automatic actions' => 'Automatische Aktionen',
- 'Your automatic action have been created successfully.' => 'Die Automatische Aktion wurde erfolgreich erstellt.',
+ '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.',
'Remove an action' => 'Aktion löschen',
'Unable to remove this action.' => 'Löschen der Aktion nicht möglich.',
@@ -221,8 +225,8 @@ return array(
'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt "%s"',
'Defined actions' => 'Definierte Aktionen',
'Add an action' => 'Aktion hinzufügen',
- 'Event name' => 'Ereignis',
- 'Action name' => 'Aktion',
+ 'Event name' => 'Ereignisname',
+ 'Action name' => 'Aktionsname',
'Action parameters' => 'Aktionsparameter',
'Action' => 'Aktion',
'Event' => 'Ereignis',
@@ -291,7 +295,7 @@ return array(
'Email address invalid' => 'Ungültige E-Mail-Adresse',
'Your Google Account is not linked anymore to your profile.' => 'Google Account nicht mehr mit dem Profil verbunden.',
'Unable to unlink your Google Account.' => 'Trennung der Verbindung zum Google Account nicht möglich.',
- 'Google authentication failed' => 'Zugang mit Google fehl geschlagen',
+ 'Google authentication failed' => 'Zugriff mit Google fehlgeschlagen',
'Unable to link your Google Account.' => 'Verbindung mit diesem Google Account nicht möglich.',
'Your Google Account is linked to your profile successfully.' => 'Der Google Account wurde erfolgreich verbunden.',
'Email' => 'E-Mail',
@@ -330,7 +334,7 @@ return array(
'Remove a file' => 'Datei löschen',
'Unable to remove this file.' => 'Löschen der Datei nicht möglich.',
'File removed successfully.' => 'Datei erfolgreich gelöscht.',
- 'Attach a document' => 'Datei anhängen',
+ 'Attach a document' => 'Dokument anhängen',
'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: "%s"?',
'open' => 'öffnen',
'Attachments' => 'Anhänge',
@@ -342,33 +346,33 @@ return array(
'Time tracking' => 'Zeiterfassung',
'Estimate:' => 'Geschätzt:',
'Spent:' => 'Aufgewendet:',
- 'Do you really want to remove this sub-task?' => 'Soll diese Unteraufgabe wirklich gelöscht werden: "%s"?',
+ 'Do you really want to remove this sub-task?' => 'Soll diese Teilaufgabe wirklich gelöscht werden: "%s"?',
'Remaining:' => 'Verbleibend:',
'hours' => 'Stunden',
'spent' => 'aufgewendet',
'estimated' => 'geschätzt',
- 'Sub-Tasks' => 'Unteraufgaben',
- 'Add a sub-task' => 'Unteraufgabe anlegen',
+ 'Sub-Tasks' => 'Teilaufgaben',
+ 'Add a sub-task' => 'Teilaufgabe anlegen',
'Original estimate' => 'Geschätzter Aufwand',
- 'Create another sub-task' => 'Weitere Unteraufgabe anlegen',
+ 'Create another sub-task' => 'Weitere Teilaufgabe anlegen',
'Time spent' => 'Aufgewendete Zeit',
- 'Edit a sub-task' => 'Unteraufgabe bearbeiten',
- 'Remove a sub-task' => 'Unteraufgabe löschen',
+ 'Edit a sub-task' => 'Teilaufgabe bearbeiten',
+ 'Remove a sub-task' => 'Teilaufgabe löschen',
'The time must be a numeric value' => 'Zeit nur als nummerische Angabe',
'Todo' => 'Nicht gestartet',
'In progress' => 'In Bearbeitung',
- 'Sub-task removed successfully.' => 'Unteraufgabe erfolgreich gelöscht.',
- 'Unable to remove this sub-task.' => 'Löschen der Unteraufgabe nicht möglich.',
- 'Sub-task updated successfully.' => 'Unteraufgabe erfolgreich aktualisiert.',
- 'Unable to update your sub-task.' => 'Aktualisieren der Unteraufgabe nicht möglich.',
- 'Unable to create your sub-task.' => 'Erstellen der Unteraufgabe nicht möglich.',
- 'Sub-task added successfully.' => 'Unteraufgabe erfolgreich angelegt.',
+ 'Sub-task removed successfully.' => 'Teilaufgabe erfolgreich gelöscht.',
+ 'Unable to remove this sub-task.' => 'Löschen der Teilaufgabe nicht möglich.',
+ 'Sub-task updated successfully.' => 'Teilaufgabe erfolgreich aktualisiert.',
+ 'Unable to update your sub-task.' => 'Aktualisieren der Teilaufgabe nicht möglich.',
+ 'Unable to create your sub-task.' => 'Erstellen der Teilaufgabe nicht möglich.',
+ 'Sub-task added successfully.' => 'Teilaufgabe erfolgreich angelegt.',
'Maximum size: ' => 'Maximalgröße: ',
'Unable to upload the file.' => 'Hochladen der Datei nicht möglich.',
- 'Display another project' => 'Zu Projekt wechseln...',
+ 'Display another project' => 'Zu Projekt wechseln',
'Your GitHub account was successfully linked to your profile.' => 'GitHub Account erfolgreich mit dem Profil verbunden.',
'Unable to link your GitHub Account.' => 'Verbindung mit diesem GitHub Account nicht möglich.',
- 'GitHub authentication failed' => 'Zugang mit GitHub fehl geschlagen',
+ 'GitHub authentication failed' => 'Zugriff mit GitHub fehlgeschlagen',
'Your GitHub account is no longer linked to your profile.' => 'GitHub Account nicht mehr mit dem Profil verbunden.',
'Unable to unlink your GitHub Account.' => 'Trennung der Verbindung zum GitHub Account nicht möglich.',
'Login with my GitHub Account' => 'Anmelden mit meinem GitHub Account',
@@ -385,8 +389,6 @@ return array(
'Creator' => 'Erstellt von',
'Modification date' => 'Änderungsdatum',
'Completion date' => 'Abschlussdatum',
- 'Webhook URL for task creation' => 'Webhook URL zur Aufgabenerstellung',
- 'Webhook URL for task modification' => 'Webhook URL zur Aufgabenbearbeitung',
'Clone' => 'duplizieren',
'Clone Project' => 'Projekt duplizieren',
'Project cloned successfully.' => 'Projekt wurde dupliziert.',
@@ -396,30 +398,28 @@ return array(
'Task position:' => 'Position der Aufgabe',
'The task #%d have been opened.' => 'Die Aufgabe #%d wurde geöffnet.',
'The task #%d have been closed.' => 'Die Aufgabe #%d wurde geschlossen.',
- 'Sub-task updated' => 'Unteraufgabe aktualisiert',
+ 'Sub-task updated' => 'Teilaufgabe aktualisiert',
'Title:' => 'Titel',
'Status:' => 'Status',
'Assignee:' => 'Zuständigkeit:',
'Time tracking:' => 'Zeittracking',
- 'New sub-task' => 'Neue Unteraufgabe',
+ 'New sub-task' => 'Neue Teilaufgabe',
'New attachment added "%s"' => 'Neuer Anhang "%s" wurde hinzugefügt.',
'Comment updated' => 'Kommentar wurde aktualisiert',
'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s',
- // 'List of due tasks for the project "%s"' => '',
- // '[%s][New attachment] %s (#%d)' => '',
- // '[%s][New comment] %s (#%d)' => '',
- // '[%s][Comment updated] %s (#%d)' => '',
- // '[%s][New subtask] %s (#%d)' => '',
- // '[%s][Subtask updated] %s (#%d)' => '',
- // '[%s][New task] %s (#%d)' => '',
- // '[%s][Task updated] %s (#%d)' => '',
- // '[%s][Task closed] %s (#%d)' => '',
- // '[%s][Task opened] %s (#%d)' => '',
- // '[%s][Due tasks]' => '',
- // '[Kanboard] Notification' => '',
+ 'List of due tasks for the project "%s"' => 'Liste der fälligen Aufgaben für das Projekt "%s"',
+ 'New attachment' => 'Neuer Anhang',
+ 'New comment' => 'Neuer Kommentar',
+ 'New subtask' => 'Neue Teilaufgabe',
+ 'Subtask updated' => 'Teilaufgabe aktualisiert',
+ 'Task updated' => 'Aufgabe aktualisiert',
+ 'Task closed' => 'Aufgabe geschlossen',
+ 'Task opened' => 'Aufgabe geöffnet',
+ '[%s][Due tasks]' => '[%s][Fällige Aufgaben]',
+ '[Kanboard] Notification' => '[Kanboard] Benachrichtigung',
'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' => 'Öffentlich',
+ 'Public access' => 'Öffentlicher Zugriff',
'Category management' => 'Kategorien verwalten',
'User management' => 'Benutzer verwalten',
'Active tasks' => 'Aktive Aufgaben',
@@ -447,12 +447,12 @@ return array(
'Username:' => 'Benutzername',
'Name:' => 'Name',
'Email:' => 'E-Mail',
- 'Default project:' => 'Standardprojekt',
- 'Notifications:' => 'Benachrichtigungen',
+ 'Default project:' => 'Standardprojekt:',
+ 'Notifications:' => 'Benachrichtigungen:',
'Notifications' => 'Benachrichtigungen',
'Group:' => 'Gruppe',
'Regular user' => 'Standardbenutzer',
- 'Account type:' => 'Accounttyp',
+ 'Account type:' => 'Accounttyp:',
'Edit profile' => 'Profil bearbeiten',
'Change password' => 'Passwort ändern',
'Password modification' => 'Passwortänderung',
@@ -467,25 +467,25 @@ return array(
'Unable to change the password.' => 'Passwort konnte nicht geändert werden.',
'Change category for the task "%s"' => 'Kategorie der Aufgabe "%s" ändern',
'Change category' => 'Kategorie ändern',
- '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> aktualisiert',
- '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> geöffnet',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> auf die Position #%d in der Spalte "%s" verschoben',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> in die Spalte "%s" verschoben',
- '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> angelegt',
- '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> geschlossen',
- '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat eine Unteraufgabe für die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> angelegt',
- '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat eine Unteraufgabe der Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> verändert',
+ '%s updated the task %s' => '%s hat die Aufgabe %s aktualisiert',
+ '%s opened the task %s' => '%s hat die Aufgabe %s geöffnet',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s hat die Aufgabe %s auf die Position #%d in der Spalte "%s" verschoben',
+ '%s moved the task %s to the column "%s"' => '%s hat die Aufgabe %s in die Spalte "%s" verschoben',
+ '%s created the task %s' => '%s hat die Aufgabe %s angelegt',
+ '%s closed the task %s' => '%s hat die Aufgabe %s geschlossen',
+ '%s created a subtask for the task %s' => '%s hat eine Teilaufgabe für die Aufgabe %s angelegt',
+ '%s updated a subtask for the task %s' => '%s hat eine Teilaufgabe der Aufgabe %s verändert',
'Assigned to %s with an estimate of %s/%sh' => 'An %s zugewiesen mit einer Schätzung von %s/%s Stunden',
'Not assigned, estimate of %sh' => 'Nicht zugewiesen, Schätzung von %s Stunden',
- '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat einen Kommentat der Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> aktualisiert',
- '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> kommentiert',
+ '%s updated a comment on the task %s' => '%s hat einen Kommentat der Aufgabe %s aktualisiert',
+ '%s commented the task %s' => '%s hat die Aufgabe %s kommentiert',
'%s\'s activity' => '%s\'s Aktivität',
'No activity.' => 'Keine Aktivität.',
- // 'RSS feed' => '',
+ 'RSS feed' => 'RSS Feed',
'%s updated a comment on the task #%d' => '%s hat einen Kommentar der Aufgabe #%d aktualisiert',
'%s commented on the task #%d' => '%s hat die Aufgabe #%d kommentiert',
- '%s updated a subtask for the task #%d' => '%s hat eine Unteraufgabe der Aufgabe #%d aktualisiert',
- '%s created a subtask for the task #%d' => '%s hat eine Unteraufgabe der Aufgabe #%d angelegt',
+ '%s updated a subtask for the task #%d' => '%s hat eine Teilaufgabe der Aufgabe #%d aktualisiert',
+ '%s created a subtask for the task #%d' => '%s hat eine Teilaufgabe der Aufgabe #%d angelegt',
'%s updated the task #%d' => '%s hat die Aufgabe #%d aktualisiert',
'%s created the task #%d' => '%s hat die Aufgabe #%d angelegt',
'%s closed the task #%d' => '%s hat die Aufgabe #%d geschlossen',
@@ -497,10 +497,10 @@ return array(
'Default columns for new projects (Comma-separated)' => 'Standardspalten für neue Projekte (komma-getrennt)',
'Task assignee change' => 'Zuständigkeit geändert',
'%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s',
- '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '%s hat die Zuständigkeit der Aufgabe <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> geändert um %s',
- '[%s][Column Change] %s (#%d)' => '[%s][Spaltenänderung] %s (#%d)',
- '[%s][Position Change] %s (#%d)' => '[%s][Positionsänderung] %s (#%d)',
- '[%s][Assignee Change] %s (#%d)' => '[%s][Zuständigkeitsänderung] %s (#%d)',
+ '%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s',
+ 'Column Change' => 'Spalte ändern',
+ 'Position Change' => 'Position ändern',
+ 'Assignee Change' => 'Zuordnung ändern',
'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"',
'Choose an event' => 'Aktion wählen',
'Github commit received' => 'Github commit empfangen',
@@ -510,7 +510,7 @@ return array(
'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' => '',
+ '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',
@@ -537,18 +537,389 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO Format wird immer akzeptiert, z.B.: "%s" und "%s"',
'New private project' => 'Neues privates Projekt',
'This project is private' => 'Dieses Projekt ist privat',
- 'Type here to create a new sub-task' => 'Hier tippen, um eine neue Unteraufgabe zu erstellen',
+ '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' => 'Geplante Zeit',
- 'There is nothing assigned to you.' => 'Es ist nichts an Sie zugewiesen.',
+ 'Time estimated' => 'Geschätzte Zeit',
+ 'There is nothing assigned to you.' => 'Ihnen ist nichts zugewiesen.',
'My tasks' => 'Meine Aufgaben',
'Activity stream' => 'Letzte Aktivitäten',
'Dashboard' => 'Dashboard',
'Confirmation' => 'Wiederholung',
- // 'Allow everybody to access to this project' => '',
- // 'Everybody have access to this project.' => '',
+ 'Allow everybody to access to this project' => 'Jedem Zugriff zu diesem Projekt gewähren',
+ 'Everybody have access to this project.' => 'Jeder hat Zugriff zu diesem Projekt',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Integration' => 'Integration',
+ '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' => 'Github Fehler Kommentar hinzugefügt',
+ 'Configure' => 'Einstellungen',
+ 'Project management' => 'Projektmanagement',
+ 'My projects' => 'Meine Projekte',
+ 'Columns' => 'Spalten',
+ 'Task' => 'Aufgabe',
+ 'Your are not member of any project.' => 'Sie sind nicht Mitglied eines Projekts.',
+ 'Percentage' => 'Prozentsatz',
+ 'Number of tasks' => 'Anzahl an Aufgaben',
+ 'Task distribution' => 'Aufgabenverteilung',
+ 'Reportings' => 'Berichte',
+ 'Task repartition for "%s"' => 'Aufgabenzuweisung für "%s"',
+ 'Analytics' => 'Analyse',
+ 'Subtask' => 'Teilaufgabe',
+ 'My subtasks' => 'Meine Teilaufgaben',
+ 'User repartition' => 'Benutzerverteilung',
+ 'User repartition for "%s"' => 'Benutzerverteilung für "%s"',
+ 'Clone this project' => 'Projekt kopieren',
+ 'Column removed successfully.' => 'Spalte erfolgreich entfernt.',
+ 'Edit Project' => 'Projekt bearbeiten',
+ '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',
+ 'The project id must be an integer' => 'Der Projektid muss eine ganze Zahl sein',
+ 'The status must be an integer' => 'Der Status muss eine ganze Zahl sein',
+ 'The subtask id is required' => 'Die Teilaufgabenid ist benötigt',
+ 'The subtask id must be an integer' => 'Die Teilaufgabenid muss eine ganze Zahl sein',
+ 'The task id is required' => 'Die Aufgabenid ist benötigt',
+ 'The task id must be an integer' => 'Die Aufgabenid muss eine ganze Zahl sein',
+ 'The user id must be an integer' => 'Die Userid muss eine ganze Zahl sein',
+ 'This value is required' => 'Dieser Wert ist erforderlich',
+ 'This value must be numeric' => 'Dieser Wert muss numerisch sein',
+ 'Unable to create this task.' => 'Diese Aufgabe kann nicht erstellt werden',
+ 'Cumulative flow diagram' => 'Kumulatives Flussdiagramm',
+ 'Cumulative flow diagram for "%s"' => 'Kumulatives Flussdiagramm für "%s"',
+ 'Daily project summary' => 'Tägliche Projektzusammenfassung',
+ 'Daily project summary export' => 'Export der täglichen Projektzusammenfassung',
+ 'Daily project summary export for "%s"' => 'Export der täglichen Projektzusammenfassung für "%s"',
+ 'Exports' => 'Exporte',
+ 'This export contains the number of tasks per column grouped per day.' => 'Dieser Export enthält die Anzahl der Aufgaben pro Spalte nach Tagen gruppiert.',
+ 'Nothing to preview...' => 'Nichts in der Vorschau anzuzeigen ...',
+ 'Preview' => 'Vorschau',
+ 'Write' => 'Ändern',
+ 'Active swimlanes' => 'Aktive Swimlane',
+ 'Add a new swimlane' => 'Eine neue Swimlane hinzufügen',
+ 'Change default swimlane' => 'Standard Swimlane ändern',
+ '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',
+ 'Swimlane removed successfully.' => 'Swimlane erfolgreich entfernt.',
+ '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)' => 'Standard Kategorien für neue Projekte (Komma-getrennt)',
+ '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' => 'Integration',
+ 'Integration with third-party services' => 'Integration von Fremdleistungen',
+ '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 Fehler',
+ 'Subtask Id' => 'Teilaufgaben Id',
+ 'Subtasks' => 'Teilaufgaben',
+ 'Subtasks Export' => 'Teilaufgaben Export',
+ 'Subtasks exportation for "%s"' => 'Teilaufgaben Export für "%s"',
+ 'Task Title' => 'Aufgaben Titel',
+ 'Untitled' => 'unbetitelt',
+ 'Application default' => 'Anwendungsstandard',
+ 'Language:' => 'Sprache:',
+ 'Timezone:' => 'Zeitzone:',
+ 'All columns' => 'Alle Spalten',
+ 'Calendar for "%s"' => 'Kalender für "%s"',
+ 'Filter by column' => 'Spalte filtern',
+ 'Filter by status' => 'Status filtern',
+ 'Calendar' => 'Kalender',
+ 'Next' => 'Nächste',
+ // '#%d' => '',
+ 'Filter by color' => 'Farbe filtern',
+ 'Filter by swimlane' => 'Swimlane filtern',
+ 'All swimlanes' => 'Alle Swimlanes',
+ 'All colors' => 'Alle Farben',
+ 'All status' => 'Alle Status',
+ 'Add a comment logging moving the task between columns' => 'Kommentar hinzufügen wenn die Aufgabe verschoben wird',
+ 'Moved to column %s' => 'In Spalte %s verschoben',
+ 'Change description' => 'Beschreibung ändern',
+ 'User dashboard' => 'Benutzer Dashboard',
+ 'Allow only one subtask in progress at the same time for a user' => 'Erlaube nur eine Teilaufgabe pro Benutzer zu bearbeiten',
+ 'Edit column "%s"' => 'Spalte "%s" bearbeiten',
+ 'Enable time tracking for subtasks' => 'Aktiviere Zeiterfassung für Teilaufgaben',
+ 'Select the new status of the subtask: "%s"' => 'Wähle einen neuen Status für Teilaufgabe: "%s"',
+ 'Subtask timesheet' => 'Teilaufgaben Zeiterfassung',
+ 'There is nothing to show.' => 'Es ist nichts zum Anzeigen vorhanden.',
+ 'Time Tracking' => 'Zeiterfassung',
+ '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?',
+ 'Change dashboard view' => 'Dashboardansicht ändern',
+ 'Show/hide activities' => 'Aktivitäten anzeigen/verbergen',
+ 'Show/hide projects' => 'Projekte anzeigen/verbergen',
+ 'Show/hide subtasks' => 'Teilaufgaben anzeigen/verbergen',
+ 'Show/hide tasks' => 'Aufgaben anzeigen/verbergen',
+ 'Disable login form' => 'Anmeldeformular deaktivieren',
+ 'Show/hide calendar' => 'Kalender anzeigen/verbergen',
+ 'User calendar' => 'Benutzer Kalender',
+ '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',
+ 'Days in this column' => 'Tage in dieser Spalte',
+ '%dd' => '%dT',
+ 'Add a link' => 'Verbindung hinzufügen',
+ 'Add a new link' => 'Neue Verbindung hinzufügen',
+ 'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?',
+ 'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?',
+ 'Field required' => 'Feld erforderlich',
+ 'Link added successfully.' => 'Verbindung erfolgreich hinzugefügt.',
+ 'Link updated successfully.' => 'Verbindung erfolgreich aktualisiert.',
+ 'Link removed successfully.' => 'Verbindung erfolgreich gelöscht.',
+ 'Link labels' => 'Verbindungsbeschriftung',
+ 'Link modification' => 'Verbindung ändern',
+ 'Links' => 'Verbindungen',
+ 'Link settings' => 'Verbindungseinstellungen',
+ 'Opposite label' => 'Gegenteil',
+ 'Remove a link' => 'Verbindung entfernen',
+ 'Task\'s links' => 'Aufgaben Verbindungen',
+ 'The labels must be different' => 'Die Beschriftung muss unterschiedlich sein',
+ 'There is no link.' => 'Es gibt keine Verbindung',
+ 'This label must be unique' => 'Die Beschriftung muss einzigartig sein',
+ 'Unable to create your link.' => 'Verbindung kann nicht erstellt werden.',
+ 'Unable to update your link.' => 'Verbindung kann nicht aktualisiert werden.',
+ 'Unable to remove this link.' => 'Verbindung kann nicht entfernt werden',
+ 'relates to' => 'gehört zu',
+ 'blocks' => 'blockiert',
+ 'is blocked by' => 'ist blockiert von',
+ 'duplicates' => 'doppelt',
+ 'is duplicated by' => 'ist gedoppelt von',
+ 'is a child of' => 'ist untergeordnet',
+ 'is a parent of' => 'ist übergeordnet',
+ 'targets milestone' => 'betrifft Meilenstein',
+ 'is a milestone of' => 'ist ein Meilenstein von',
+ 'fixes' => 'behebt',
+ 'is fixed by' => 'wird behoben von',
+ 'This task' => 'Diese Aufgabe',
+ '<1h' => '<1Std',
+ '%dh' => '%dStd',
+ // '%b %e' => '',
+ 'Expand tasks' => 'Aufgaben aufklappen',
+ 'Collapse tasks' => 'Aufgaben zusammenklappen',
+ 'Expand/collapse tasks' => 'Aufgaben auf/zuklappen',
+ 'Close dialog box' => 'Dialog schließen',
+ 'Submit a form' => 'Formular abschicken',
+ 'Board view' => 'Pinnwand Ansicht',
+ 'Keyboard shortcuts' => 'Tastaturkürzel',
+ 'Open board switcher' => 'Pinnwandauswahl öffnen',
+ 'Application' => 'Anwendung',
+ 'Filter recently updated' => 'Zuletzt geänderte anzeigen',
+ 'since %B %e, %Y at %k:%M %p' => 'seit %B %e, %Y um %k:%M %p',
+ 'More filters' => 'Mehr Filter',
+ 'Compact view' => 'Kompaktansicht',
+ 'Horizontal scrolling' => 'Horizontales Scrollen',
+ 'Compact/wide view' => 'Kompakt/Breite-Ansicht',
+ 'No results match:' => 'Keine Ergebnisse:',
+ 'Remove hourly rate' => 'Stundensatz entfernen',
+ 'Do you really want to remove this hourly rate?' => 'Diesen Stundensatz wirklich entfernen?',
+ 'Hourly rates' => 'Stundensätze',
+ 'Hourly rate' => 'Stundensatz',
+ 'Currency' => 'Währung',
+ 'Effective date' => 'Inkraftsetzung',
+ 'Add new rate' => 'Neue Rate hinzufügen',
+ 'Rate removed successfully.' => 'Rate erfolgreich entfernt',
+ 'Unable to remove this rate.' => 'Nicht in der Lage, diese Rate zu entfernen.',
+ 'Unable to save the hourly rate.' => 'Nicht in der Lage, diese Rate zu speichern',
+ 'Hourly rate created successfully.' => 'Stundensatz erfolgreich angelegt.',
+ 'Start time' => 'Startzeit',
+ 'End time' => 'Endzeit',
+ 'Comment' => 'Kommentar',
+ 'All day' => 'ganztägig',
+ 'Day' => 'Tag',
+ 'Manage timetable' => 'Zeitplan verwalten',
+ 'Overtime timetable' => 'Überstunden Zeitplan',
+ 'Time off timetable' => 'Freizeit Zeitplan',
+ 'Timetable' => 'Zeitplan',
+ 'Work timetable' => 'Arbeitszeitplan',
+ 'Week timetable' => 'Wochenzeitplan',
+ 'Day timetable' => 'Tageszeitplan',
+ 'From' => 'von',
+ 'To' => 'bis',
+ 'Time slot created successfully.' => 'Zeitfenster erfolgreich erstellt.',
+ 'Unable to save this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu speichern.',
+ 'Time slot removed successfully.' => 'Zeitfenster erfolgreich entfernt.',
+ 'Unable to remove this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu entfernen',
+ 'Do you really want to remove this time slot?' => 'Soll diese Zeitfenster wirklich gelöscht werden?',
+ 'Remove time slot' => 'Zeitfenster entfernen',
+ 'Add new time slot' => 'Neues Zeitfenster hinzufügen',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Dieses Zeitfenster wird verwendet, wenn die Checkbox "gantägig" für Freizeit und Überstunden angeklickt ist.',
+ 'Files' => 'Dateien',
+ 'Images' => 'Bilder',
+ 'Private project' => 'privates Projekt',
+ 'Amount' => 'Betrag',
+ // 'AUD - Australian Dollar' => '',
+ 'Budget' => 'Budget',
+ 'Budget line' => 'Budgetlinie',
+ 'Budget line removed successfully.' => 'Budgetlinie erfolgreich entfernt',
+ 'Budget lines' => 'Budgetlinien',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ 'Cost' => 'Kosten',
+ 'Cost breakdown' => 'Kostenaufschlüsselung',
+ 'Custom Stylesheet' => 'benutzerdefiniertes Stylesheet',
+ 'download' => 'Download',
+ 'Do you really want to remove this budget line?' => 'Soll diese Budgetlinie wirklich entfernt werden?',
+ 'EUR - Euro' => 'EUR - Euro',
+ 'Expenses' => 'Kosten',
+ 'GBP - British Pound' => 'GBP - Britische Pfung',
+ 'INR - Indian Rupee' => 'INR - Indische Rupien',
+ 'JPY - Japanese Yen' => 'JPY - Japanischer Yen',
+ 'New budget line' => 'Neue Budgetlinie',
+ 'NZD - New Zealand Dollar' => 'NZD - Neuseeland-Dollar',
+ 'Remove a budget line' => 'Budgetlinie entfernen',
+ 'Remove budget line' => 'Budgetlinie entfernen',
+ 'RSD - Serbian dinar' => 'RSD - Serbische Dinar',
+ 'The budget line have been created successfully.' => 'Die Budgetlinie wurde erfolgreich angelegt.',
+ 'Unable to create the budget line.' => 'Budgetlinie konnte nicht erstellt werden.',
+ 'Unable to remove this budget line.' => 'Budgetlinie konnte nicht gelöscht werden.',
+ 'USD - US Dollar' => 'USD - US Dollar',
+ 'Remaining' => 'Verbleibend',
+ 'Destination column' => 'Zielspalte',
+ 'Move the task to another column when assigned to a user' => 'Aufgabe in eine andere Spalte verschieben, wenn ein User zugeordnet wurde.',
+ 'Move the task to another column when assignee is cleared' => 'Aufgabe in eine andere Spalte verschieben, wenn die Zuordnung gelöscht wurde.',
+ 'Source column' => 'Quellspalte',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ 'Transitions' => 'Übergänge',
+ 'Executer' => 'Ausführender',
+ 'Time spent in the column' => 'Zeit in Spalte verbracht',
+ 'Task transitions' => 'Aufgaben Übergänge',
+ 'Task transitions export' => 'Aufgaben Übergänge exportieren',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Diese Auswertung enthält alle Spaltenbewegungen für jede Aufgabe mit Datum, Benutzer und Zeit vor jedem Wechsel.',
+ 'Currency rates' => 'Währungskurse',
+ 'Rate' => 'Kurse',
+ 'Change reference currency' => 'Referenzwährung ändern',
+ 'Add a new currency rate' => 'Neuen Währungskurs hinzufügen',
+ 'Currency rates are used to calculate project budget.' => 'Währungskurse werden verwendet um das Projektbudget zu berechnen.',
+ 'Reference currency' => 'Referenzwährung',
+ 'The currency rate have been added successfully.' => 'Der Währungskurs wurde erfolgreich hinzugefügt.',
+ 'Unable to add this currency rate.' => 'Währungskurs konnte nicht hinzugefügt werden',
+ 'Send notifications to a Slack channel' => 'Benachrichtigung an einen Slack-Kanal senden',
+ 'Webhook URL' => 'Webhook URL',
+ 'Help on Slack integration' => 'Hilfe für Slack integration.',
+ '%s remove the assignee of the task %s' => '%s Zuordnung für die Aufgabe %s entfernen',
+ 'Send notifications to Hipchat' => 'Sende Benachrichtigung an Hipchat',
+ 'API URL' => 'API URL',
+ 'Room API ID or name' => 'Raum API ID oder Name',
+ 'Room notification token' => 'Raum Benachrichtigungstoken',
+ 'Help on Hipchat integration' => 'Hilfe bei Hipchat Integration',
+ 'Enable Gravatar images' => 'Aktiviere Gravatar Bilder',
+ 'Information' => 'Information',
+ 'Check two factor authentication code' => 'Prüfe Zwei-Faktor-Authentifizierungscode',
+ 'The two factor authentication code is not valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist ungültig.',
+ '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-Chart für "%s"',
+ 'Burndown chart' => 'Burndown-Chart',
+ 'This chart show the task complexity over the time (Work Remaining).' => 'Dieses Diagramm zeigt die Aufgabenkomplexität über den Faktor Zeit (Verbleibende Arbeit).',
+ 'Screenshot taken %s' => 'Screenshot aufgenommen %s ',
+ 'Add a screenshot' => 'Füge einen Screenshot hinzu',
+ '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',
+ 'Postmark (incoming emails)' => 'Postmark (Eingehende E-Mails)',
+ 'Help on Postmark integration' => 'Hilfe bei Postmark-Integration',
+ 'Mailgun (incoming emails)' => 'Mailgun (Eingehende E-Mails)',
+ 'Help on Mailgun integration' => 'Hilfe bei Mailgun-Integration',
+ 'Sendgrid (incoming emails)' => 'Sendgrid (Eingehende E-Mails)',
+ 'Help on Sendgrid integration' => 'Hilfe bei Sendgrid-Integration',
+ '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"?',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
);
diff --git a/app/Locales/es_ES/translations.php b/app/Locale/es_ES/translations.php
index d24cdfcf..2c215390 100644
--- a/app/Locales/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -1,6 +1,8 @@
<?php
return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => '.',
'None' => 'Ninguno',
'edit' => 'modificar',
'Edit' => 'Modificar',
@@ -83,7 +85,7 @@ return array(
'Settings' => 'Preferencias',
'Application settings' => 'Parámetros de la aplicación',
'Language' => 'Idioma',
- 'Webhook token:' => 'Ficha de seguridad (token) para los webhooks :',
+ 'Webhook token:' => 'Ficha de seguridad (token) para los disparadores Web (webhooks):',
'API token:' => 'Ficha de seguridad (token) para API:',
'More information' => 'Más informaciones',
'Database size:' => 'Tamaño de la base de datos:',
@@ -182,21 +184,22 @@ return array(
'Change assignee' => 'Cambiar la persona asignada',
'Change assignee for the task "%s"' => 'Cambiar la persona asignada por la tarea « %s »',
'Timezone' => 'Zona horaria',
- 'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!',
+ 'Sorry, I didn\'t find this information in my database!' => 'Lo siento no he encontrado información en la base de datos!',
'Page not found' => 'Página no encontrada',
'Complexity' => 'Complejidad',
'limit' => 'límite',
'Task limit' => 'Número máximo de tareas',
+ 'Task count' => 'Contador de tareas',
'This value must be greater than %d' => 'Este valor no debe de ser más grande que %d',
'Edit project access list' => 'Editar los permisos del proyecto',
'Edit users access' => 'Editar los permisos de usuario',
'Allow this user' => 'Autorizar este usuario',
'Only those users have access to this project:' => 'Solo estos usuarios tienen acceso a este proyecto:',
'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.',
- 'revoke' => 'revocar',
+ 'Revoke' => 'Revocar',
'List of authorized users' => 'Lista de los usuarios autorizados',
'User' => 'Usuario',
- // 'Nobody have access to this project.' => '',
+ 'Nobody have access to this project.' => 'Nadie tiene acceso a este proyecto',
'You are not allowed to access to this project.' => 'No está autorizado a acceder a este proyecto.',
'Comments' => 'Comentarios',
'Post comment' => 'Commentar',
@@ -212,6 +215,7 @@ return array(
'Invalid date' => 'Fecha no válida',
'Must be done before %B %e, %Y' => 'Debe de estar hecho antes del %d/%m/%Y',
'%B %e, %Y' => '%d/%m/%Y',
+ // '%b %e, %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.',
@@ -385,8 +389,6 @@ return array(
'Creator' => 'Creador',
'Modification date' => 'Fecha de modificación',
'Completion date' => 'Fecha de terminación',
- 'Webhook URL for task creation' => 'Webhook para la creación de tareas',
- 'Webhook URL for task modification' => 'Webhook para la modificación de tareas',
'Clone' => 'Clonar',
'Clone Project' => 'Clonar proyecto',
'Project cloned successfully.' => 'Proyecto clonado correctamente',
@@ -406,15 +408,13 @@ return array(
'Comment updated' => 'Comentario actualizado',
'New comment posted by %s' => 'Nuevo comentario agregado por %s',
'List of due tasks for the project "%s"' => 'Lista de tareas para el proyecto "%s"',
- '[%s][New attachment] %s (#%d)' => '[%s][uevo adjunto] %s (#%d)',
- '[%s][New comment] %s (#%d)' => '[%s][Nuevo comentario] %s (#%d)',
- '[%s][Comment updated] %s (#%d)' => '[%s][Comentario actualizado] %s (#%d)',
- '[%s][New subtask] %s (#%d)' => '[%s][Nueva subtarea] %s (#%d)',
- '[%s][Subtask updated] %s (#%d)' => '[%s][Subtarea actualizada] %s (#%d)',
- '[%s][New task] %s (#%d)' => '[%s][Nueva tarea] %s (#%d)',
- '[%s][Task updated] %s (#%d)' => '[%s][Tarea actualizada] %s (#%d)',
- '[%s][Task closed] %s (#%d)' => '[%s][Tarea cerrada] %s (#%d)',
- '[%s][Task opened] %s (#%d)' => '[%s][Tarea abierta] %s (#%d)',
+ 'New attachment' => 'Nuevo adjunto',
+ 'New comment' => 'Nuevo comentario',
+ 'New subtask' => 'Nueva subtarea',
+ 'Subtask updated' => 'Subtarea actualizada',
+ 'Task updated' => 'Tarea actualizada',
+ 'Task closed' => 'Tarea cerrada',
+ 'Task opened' => 'Tarea abierta',
'[%s][Due tasks]' => '[%s][Tareas vencidas]',
'[Kanboard] Notification' => '[Kanboard] Notificación',
'I want to receive notifications only for those projects:' => 'Quiero recibir notificaciones sólo de estos proyectos:',
@@ -449,9 +449,9 @@ return array(
'Email:' => 'Correo electrónico:',
'Default project:' => 'Proyecto por defecto:',
'Notifications:' => 'Notificaciones:',
- // 'Notifications' => '',
+ 'Notifications' => 'Notificaciones',
'Group:' => 'Grupo:',
- 'Regular user' => 'Usuario regular:',
+ 'Regular user' => 'Usuario regular',
'Account type:' => 'Tipo de Cuenta:',
'Edit profile' => 'Editar perfil',
'Change password' => 'Cambiar contraseña',
@@ -467,18 +467,18 @@ return array(
'Unable to change the password.' => 'No pude cambiar la contraseña.',
'Change category for the task "%s"' => 'Cambiar la categoría de la tarea "%s"',
'Change category' => 'Cambiar categoría',
- '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s actualizó la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s abrió la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s movió la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> a la posición #%d de la columna "%s"',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s movió la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> a la columna "%s"',
- '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s creó la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s cerró la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s creó una subtarea para la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s actualizó una subtarea para la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
+ '%s updated the task %s' => '%s actualizó la tarea %s',
+ '%s opened the task %s' => '%s abrió la tarea %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s movió la tarea %s a la posición #%d de la columna "%s"',
+ '%s moved the task %s to the column "%s"' => '%s movió la tarea %s a la columna "%s"',
+ '%s created the task %s' => '%s creó la tarea %s',
+ '%s closed the task %s' => '%s cerró la tarea %s',
+ '%s created a subtask for the task %s' => '%s creó una subtarea para la tarea %s',
+ '%s updated a subtask for the task %s' => '%s actualizó una subtarea para la tarea %s',
'Assigned to %s with an estimate of %s/%sh' => 'Asignada a %s con una estimación de %s/%sh',
'Not assigned, estimate of %sh' => 'No asignada, se estima en %sh',
- '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s actualizó un comentario de la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s comentó la tarea <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
+ '%s updated a comment on the task %s' => '%s actualizó un comentario de la tarea %s',
+ '%s commented the task %s' => '%s comentó la tarea %s',
'%s\'s activity' => 'Actividad de %s',
'No activity.' => 'Sin actividad',
'RSS feed' => 'Fichero RSS',
@@ -496,59 +496,430 @@ return array(
'Default values are "%s"' => 'Los valores por defecto son "%s"',
'Default columns for new projects (Comma-separated)' => 'Columnas por defecto de los nuevos proyectos (Separadas mediante comas)',
'Task assignee change' => 'Cambiar persona asignada a la tarea',
- // '%s change the assignee of the task #%d to %s' => '',
- // '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '',
- '[%s][Column Change] %s (#%d)' => '[%s][Cambia Columna] %s (#%d)',
- '[%s][Position Change] %s (#%d)' => '[%s][Cambia Posición] %s (#%d)',
- '[%s][Assignee Change] %s (#%d)' => '[%s][Cambia Persona Asignada] %s (#%d)',
+ '%s change the assignee of the task #%d to %s' => '%s cambió el asignado de la tarea #%d por %s',
+ '%s changed the assignee of the task %s to %s' => '%s cambió el asignado de la tarea %s por %s',
+ 'Column Change' => 'Cambio de Columna',
+ 'Position Change' => 'Cambio de Posición',
+ 'Assignee Change' => 'Cambio de Asignado',
'New password for the user "%s"' => 'Nueva contraseña para el usuario "%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' => '',
- // 'Label' => '',
- // 'Database' => '',
- // 'About' => '',
- // 'Database driver:' => '',
- // 'Board settings' => '',
- // 'URL and token' => '',
- // 'Webhook settings' => '',
- // 'URL for task creation:' => '',
- // 'Reset token' => '',
- // 'API endpoint:' => '',
- // 'Refresh interval for private board' => '',
- // 'Refresh interval for public board' => '',
- // 'Task highlight period' => '',
- // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
- // 'Frequency in second (60 seconds by default)' => '',
- // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
- // 'Application URL' => '',
- // 'Example: http://example.kanboard.net/ (used by email notifications)' => '',
- // 'Token regenerated.' => '',
- // 'Date format' => '',
- // 'ISO format is always accepted, example: "%s" and "%s"' => '',
- // 'New private project' => '',
- // 'This project is private' => '',
- // 'Type here to create a new sub-task' => '',
- // 'Add' => '',
- // 'Estimated time: %s hours' => '',
- // 'Time spent: %s hours' => '',
- // 'Started on %B %e, %Y' => '',
- // '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.' => '',
+ 'Choose an event' => 'Escoga un evento',
+ 'Github commit received' => 'Envío a Github recibido',
+ 'Github issue opened' => 'Problema en Github abierto',
+ 'Github issue closed' => 'Problema en Github cerrado',
+ 'Github issue reopened' => 'Problema en Github reabierto',
+ 'Github issue assignee change' => 'Cambio en signación de problema en Github',
+ 'Github issue label change' => 'Cambio en etiqueta del problema',
+ '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 la asignación 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',
+ 'Database driver:' => 'Driver de la base de datos',
+ 'Board settings' => 'Configuraciones del Tablero',
+ 'URL and token' => 'URL y token',
+ 'Webhook settings' => 'Configuraciones del Disparador Web (Webhook)',
+ 'URL for task creation:' => 'URL para la creación de tareas',
+ 'Reset token' => 'Resetear token',
+ 'API endpoint:' => 'Punto final del API',
+ 'Refresh interval for private board' => 'Intervalo de refrescamiento del tablero privado',
+ 'Refresh interval for public board' => 'Intervalo de refrescamiento del tablero público',
+ 'Task highlight period' => 'Periodo del realce de la tarea',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periodo (en segundos) para considerar que una tarea fué modificada recientemente (0 para deshabilitar, 2 días por defecto)',
+ '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.' => 'Token regenerado',
+ '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"',
+ 'New private project' => 'Nuevo proyecto privado',
+ 'This project is private' => 'Este proyecto es privado',
+ 'Type here to create a new sub-task' => 'Escriba aquí para crear una nueva sub-tarea',
+ 'Add' => 'Añadir',
+ 'Estimated time: %s hours' => 'Tiempo estimado: % horas',
+ 'Time spent: %s hours' => 'Tiempo invertido: %s horas',
+ 'Started on %B %e, %Y' => 'Iniciado en %B %e, %Y',
+ 'Start date' => 'Fecha de inicio',
+ 'Time estimated' => 'Tiempo estimado',
+ 'There is nothing assigned to you.' => 'Esto no le está asignado',
+ 'My tasks' => 'Mis tareas',
+ 'Activity stream' => 'Flujo de actividad',
+ 'Dashboard' => 'Tablero',
+ 'Confirmation' => 'Confirmación',
+ 'Allow everybody to access to this project' => 'Permitir a cualquier acceder a este proyecto',
+ 'Everybody have access to this project.' => 'Cualquier tiene acceso a este proyecto',
+ 'Webhooks' => 'Disparadores Web (Webhooks)',
+ 'API' => 'API',
+ 'Integration' => 'Integración',
+ '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',
+ 'Configure' => 'Configurar',
+ 'Project management' => 'Administración del proyecto',
+ 'My projects' => 'Mis proyectos',
+ 'Columns' => 'Columnas',
+ 'Task' => 'Tarea',
+ 'Your are not member of any project.' => 'No es miembro de ningún proyecto',
+ 'Percentage' => 'Porcentaje',
+ 'Number of tasks' => 'Número de tareas',
+ 'Task distribution' => 'Distribución de tareas',
+ 'Reportings' => 'Reportes',
+ 'Task repartition for "%s"' => 'Repartición de tareas para "%s"',
+ 'Analytics' => 'Analítica',
+ 'Subtask' => 'Subtarea',
+ 'My subtasks' => 'Mis subtareas',
+ 'User repartition' => 'Repartición de usuarios',
+ 'User repartition for "%s"' => 'Repartición para "%s"',
+ 'Clone this project' => 'Clonar este proyecto',
+ 'Column removed successfully.' => 'Columna removida correctamente',
+ 'Edit Project' => 'Editar Proyecto',
+ 'Github Issue' => 'Problema 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',
+ 'The project id must be an integer' => 'El id del proyecto debe ser un entero',
+ 'The status must be an integer' => 'El estado debe ser un entero',
+ 'The subtask id is required' => 'El id de la subtarea es requerido',
+ 'The subtask id must be an integer' => 'El id de la subtarea debe ser un entero',
+ 'The task id is required' => 'El id de la tarea es requerido',
+ 'The task id must be an integer' => 'El id de la tarea debe ser un entero',
+ 'The user id must be an integer' => 'El id del usuario debe ser un entero',
+ 'This value is required' => 'El valor es requerido',
+ 'This value must be numeric' => 'Este valor debe ser numérico',
+ 'Unable to create this task.' => 'Imposible crear esta tarea',
+ 'Cumulative flow diagram' => 'Diagrama de flujo acumulativo',
+ 'Cumulative flow diagram for "%s"' => 'Diagrama de flujo acumulativo para "%s"',
+ 'Daily project summary' => 'Sumario diario del proyecto',
+ 'Daily project summary export' => 'Exportar sumario diario del proyecto',
+ 'Daily project summary export for "%s"' => 'Exportar sumario diario del proyecto para "%s"',
+ 'Exports' => 'Exportar',
+ 'This export contains the number of tasks per column grouped per day.' => 'Esta exportación contiene el número de tereas por columna agrupada por día',
+ 'Nothing to preview...' => 'Nada que previsualizar...',
+ 'Preview' => 'Previsualizar',
+ 'Write' => 'Escribir',
+ 'Active swimlanes' => 'Carriles activos',
+ 'Add a new swimlane' => 'Añadir nuevo carril',
+ 'Change default swimlane' => 'Cambiar el carril por defecto',
+ 'Default swimlane' => 'Carril por defecto',
+ 'Do you really want to remove this swimlane: "%s"?' => '¿Realmente quiere remover este carril: "%s"?',
+ 'Inactive swimlanes' => 'Carriles inactivos',
+ 'Set project manager' => 'Asignar administrador del proyecto',
+ 'Set project member' => 'Asignar miembro del proyecto',
+ 'Remove a swimlane' => 'Remover un carril',
+ 'Rename' => 'Renombrar',
+ 'Show default swimlane' => 'Mostrar carril por defecto',
+ // 'Swimlane modification for the project "%s"' => '',
+ 'Swimlane not found.' => 'Carril no encontrado',
+ 'Swimlane removed successfully.' => 'Carril removido correctamente',
+ 'Swimlanes' => 'Carriles',
+ 'Swimlane updated successfully.' => 'Carril actualizado correctamente',
+ 'The default swimlane have been updated successfully.' => 'El carril por defecto ha sido actualizado correctamente',
+ 'Unable to create your swimlane.' => 'Imposible crear su carril',
+ 'Unable to remove this swimlane.' => 'Imposible remover este carril',
+ 'Unable to update this swimlane.' => 'Imposible actualizar este carril',
+ 'Your swimlane have been created successfully.' => 'Su carril ha sido creado 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' => 'Gestor 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 gestor 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',
+ 'Subtasks exportation for "%s"' => 'Exportación de subtareas para "%s"',
+ 'Task Title' => 'Título de la tarea',
+ 'Untitled' => 'Sin título',
+ 'Application default' => 'Predefinido de la aplicación',
+ 'Language:' => 'Idioma',
+ 'Timezone:' => 'Zona horaria',
+ 'All columns' => 'Todas las columnas',
+ 'Calendar for "%s"' => 'Calendario para "%s"',
+ 'Filter by column' => 'Filtrar por columna',
+ 'Filter by status' => 'Filtrar por estado',
+ 'Calendar' => 'Calendario',
+ 'Next' => 'Siguiente',
+ // '#%d' => '',
+ 'Filter by color' => 'Filtrar por color',
+ 'Filter by swimlane' => 'Filtrar por carril',
+ 'All swimlanes' => 'Todos los carriles',
+ 'All colors' => 'Todos los colores',
+ 'All status' => 'Todos los estados',
+ 'Add a comment logging moving the task between columns' => 'Añadir un cometario de historial moviendo la tarea entre columnas',
+ 'Moved to column %s' => 'Movido a columna %s',
+ 'Change description' => 'Cambiar descripción',
+ 'User dashboard' => 'Tablero de usuario',
+ 'Allow only one subtask in progress at the same time for a user' => 'Permitir sólo una subtarea en progreso a la vez para cada usuario',
+ 'Edit column "%s"' => 'Editar columna %s',
+ 'Enable time tracking for subtasks' => 'Activar seguimiento temporal para subtareas',
+ 'Select the new status of the subtask: "%s"' => 'Seleccionar el nuevo estado de la subtarea: "%s"',
+ 'Subtask timesheet' => 'Hoja temporal de subtarea',
+ 'There is nothing to show.' => 'Nada que mostrar',
+ 'Time Tracking' => 'Seguimiento Temporal',
+ '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 deseas duplicar?',
+ 'Change dashboard view' => 'Cambiar vista de tablero',
+ 'Show/hide activities' => 'Mostrar/ocultar actividades',
+ 'Show/hide projects' => 'Mostrar/ocultar proyectos',
+ 'Show/hide subtasks' => 'Mostrar/Ocultar subtareas',
+ 'Show/hide tasks' => 'Mostrar/ocultar tareas',
+ 'Disable login form' => 'Desactivar formulario de ingreso',
+ 'Show/hide calendar' => 'Mostrar/ocultar calendario',
+ 'User calendar' => 'Calendario de usuario',
+ 'Bitbucket commit received' => 'Recibida 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',
+ 'Days in this column' => 'Días en esta columna',
+ // '%dd' => '',
+ 'Add a link' => 'Añadir enlace',
+ 'Add a new link' => 'Añadir nuevo enlace',
+ 'Do you really want to remove this link: "%s"?' => '¿Realmente quieres quitar este enlace: "%s"?',
+ 'Do you really want to remove this link with task #%d?' => '¿Realmente quieres quitar este enlace con esta tarea: #%d?',
+ 'Field required' => 'Es necesario el campo',
+ 'Link added successfully.' => 'Enlace añadido con éxito.',
+ 'Link updated successfully.' => 'Enlace actualizado con éxito',
+ 'Link removed successfully.' => 'Enlace quitado con éxito',
+ 'Link labels' => 'etiquetas de enlace',
+ 'Link modification' => 'Modificación de enlace',
+ 'Links' => 'Enlaces',
+ 'Link settings' => 'Preferencias de enlace',
+ 'Opposite label' => 'Etiqueta opuesta',
+ 'Remove a link' => 'Quitar un enlace',
+ 'Task\'s links' => 'Enlaces de tareas',
+ 'The labels must be different' => 'Las etiquetas han de ser diferentes',
+ 'There is no link.' => 'No hay enlace',
+ 'This label must be unique' => 'Esta etiqueta ha de ser única',
+ 'Unable to create your link.' => 'No puedo crea su enlace.',
+ 'Unable to update your link.' => 'No puedo actualizar su enlace.',
+ 'Unable to remove this link.' => 'No puedo quitar este enlace.',
+ 'relates to' => 'se refiere a',
+ 'blocks' => 'bloques',
+ 'is blocked by' => 'bloqueado por',
+ 'duplicates' => 'duplica',
+ 'is duplicated by' => 'está duplicado por',
+ 'is a child of' => 'es un hijo de',
+ 'is a parent of' => 'es un padre de',
+ 'targets milestone' => 'hito de objetivos',
+ 'is a milestone of' => 'es un hito de',
+ 'fixes' => 'arregla',
+ 'is fixed by' => 'arreglado por',
+ 'This task' => 'Esta tarea',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ 'Expand tasks' => 'Espande tareas',
+ 'Collapse tasks' => 'Colapsa tareas',
+ 'Expand/collapse tasks' => 'Expande/colapasa tareas',
+ 'Close dialog box' => 'Cerrar caja de diálogo',
+ 'Submit a form' => 'Enviar formulario',
+ 'Board view' => 'Vista de tablero',
+ 'Keyboard shortcuts' => 'Atajos de teclado',
+ 'Open board switcher' => 'Abrir conmutador de tablero',
+ 'Application' => 'Aplicación',
+ 'Filter recently updated' => 'Filtro actualizado recientemente',
+ 'since %B %e, %Y at %k:%M %p' => 'desde %B %e, %Y a las %k:%M %p',
+ 'More filters' => 'Más filtros',
+ 'Compact view' => 'Compactar vista',
+ 'Horizontal scrolling' => 'Desplazamiento horizontal',
+ 'Compact/wide view' => 'Vista compacta/amplia',
+ 'No results match:' => 'No hay resultados coincidentes:',
+ 'Remove hourly rate' => 'Quitar cobro horario',
+ 'Do you really want to remove this hourly rate?' => '¿Realmente quires quitar el cobro horario?',
+ 'Hourly rates' => 'Cobros horarios',
+ 'Hourly rate' => 'Cobro horario',
+ 'Currency' => 'Moneda',
+ 'Effective date' => 'Fecha efectiva',
+ 'Add new rate' => 'Añadir nuevo cobro',
+ 'Rate removed successfully.' => 'Cobro quitado con éxito.',
+ 'Unable to remove this rate.' => 'No pude quitar este cobro.',
+ 'Unable to save the hourly rate.' => 'No pude grabar el cobro horario.',
+ 'Hourly rate created successfully.' => 'Cobro horario creado con éxito',
+ 'Start time' => 'Tiempo de inicio',
+ 'End time' => 'Tiempo de fin',
+ 'Comment' => 'Comentario',
+ 'All day' => 'Todos los días',
+ 'Day' => 'Día',
+ 'Manage timetable' => 'Gestionar horario',
+ 'Overtime timetable' => 'Horario de tiempo extra',
+ 'Time off timetable' => 'Horario de tiempo libre',
+ 'Timetable' => 'Horario',
+ 'Work timetable' => 'Horario de trabajo',
+ 'Week timetable' => 'Horario semanal',
+ 'Day timetable' => 'Horario de día',
+ 'From' => 'Desde',
+ 'To' => 'Hasta',
+ 'Time slot created successfully.' => 'Tiempo asignado creado con éxito.',
+ 'Unable to save this time slot.' => 'No pude grabar este tiempo asignado.',
+ 'Time slot removed successfully.' => 'Tiempo asignado quitado con éxito.',
+ 'Unable to remove this time slot.' => 'No pude quitar este tiempo asignado.',
+ 'Do you really want to remove this time slot?' => '¿Realmente quieres quitar este tiempo asignado?',
+ 'Remove time slot' => 'Quitar tiempo asignado',
+ 'Add new time slot' => 'Añadir nuevo tiempo asignado',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Este horario se usa cuando se marca la casilla "todos los días" para calendario de tiempo libre y horas extras.',
+ 'Files' => 'Ficheros',
+ 'Images' => 'Imágenes',
+ 'Private project' => 'Proyecto privado',
+ 'Amount' => 'Cantidad',
+ 'AUD - Australian Dollar' => 'AUD - Dólar australiano',
+ 'Budget' => 'Presupuesto',
+ 'Budget line' => 'Línea de presupuesto',
+ 'Budget line removed successfully.' => 'Línea de presupuesto quitada con éxito',
+ 'Budget lines' => 'Líneas de presupuesto',
+ 'CAD - Canadian Dollar' => 'CAD - Dólar canadiense',
+ 'CHF - Swiss Francs' => 'CHF - Francos suizos',
+ 'Cost' => 'Costo',
+ 'Cost breakdown' => 'Desglose de costes',
+ 'Custom Stylesheet' => 'Hoja de Estilo habitual',
+ 'download' => 'descargar',
+ 'Do you really want to remove this budget line?' => '¿Realmente quieres quitar esta línea de presupuesto?',
+ 'EUR - Euro' => 'EUR - Euro',
+ 'Expenses' => 'Gastos',
+ 'GBP - British Pound' => 'GBP - Libra británica',
+ 'INR - Indian Rupee' => 'INR - Rupias indúes',
+ 'JPY - Japanese Yen' => 'JPY - Yen japonés',
+ 'New budget line' => 'Nueva línea de presupuesto',
+ 'NZD - New Zealand Dollar' => 'NZD - Dóloar neocelandés',
+ 'Remove a budget line' => 'Quitar una línea de presupuesto',
+ 'Remove budget line' => 'Quitar línea de presupuesto',
+ 'RSD - Serbian dinar' => 'RSD - Dinar serbio',
+ 'The budget line have been created successfully.' => 'Se ha creado la línea de presupuesto con éxito.',
+ 'Unable to create the budget line.' => 'No pude crear la línea de presupuesto.',
+ 'Unable to remove this budget line.' => 'No pude quitar esta línea de presupuesto.',
+ 'USD - US Dollar' => 'USD - Dólar americano',
+ 'Remaining' => 'Restante',
+ 'Destination column' => 'Columna destino',
+ 'Move the task to another column when assigned to a user' => 'Mover la tarea a otra columna al asignarse al usuario',
+ 'Move the task to another column when assignee is cleared' => 'Mover la tarea a otra columna al quitar el asignado',
+ 'Source column' => 'Columna fuente',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ 'Transitions' => 'Transiciones',
+ 'Executer' => 'Ejecutor',
+ 'Time spent in the column' => 'Tiempo transcurrido en la columna',
+ 'Task transitions' => 'Transiciones de tarea',
+ 'Task transitions export' => 'Eportar transiciones de tarea',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Este informe contiene todos los movimientos de columna para cada tarea con la fecha, el usuario y el tiempo transcurrido en cada trasición.',
+ 'Currency rates' => 'Cambio de monedas',
+ 'Rate' => 'Cambio',
+ 'Change reference currency' => 'Cambiar moneda de referencia',
+ 'Add a new currency rate' => 'Añadir nuevo cambio de moneda',
+ 'Currency rates are used to calculate project budget.' => 'Se usan los cambios de moneda para calcular el presupuesto del proyecto.',
+ 'Reference currency' => 'Moneda de referencia',
+ 'The currency rate have been added successfully.' => 'Se ha añadido el cambio de moneda con éxito',
+ 'Unable to add this currency rate.' => 'No pude añadir este cambio de moneda.',
+ 'Send notifications to a Slack channel' => 'Enviar notificaciones a un canal Desatendido',
+ 'Webhook URL' => 'URL de Disparador Web (webhook)',
+ 'Help on Slack integration' => 'Ayuda sobre integración Desatendida',
+ '%s remove the assignee of the task %s' => '%s quita el asignado de la tarea %s',
+ 'Send notifications to Hipchat' => 'Enviar notificaciones a Hipchat',
+ 'API URL' => 'URL de API',
+ 'Room API ID or name' => 'ID de API de habitación o nombre',
+ 'Room notification token' => 'Notificación de ficha de Habitación',
+ 'Help on Hipchat integration' => 'Ayuda sobre integración de Hipchat',
+ 'Enable Gravatar images' => 'Activar imágenes Gravatar',
+ 'Information' => 'Información',
+ 'Check two factor authentication code' => 'Revisar código de autenticación de dos factores',
+ 'The two factor authentication code is not valid.' => 'El código de autenticación de dos factores no es válido',
+ '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 tu software TOTP (por ejemplo Autenticación Google o FreeOTP).',
+ 'Check my code' => 'Revisar mi código',
+ 'Secret key: ' => 'Clave secreta: ',
+ 'Test your device' => 'Probar tu 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)',
+ 'Screenshot taken %s' => 'Pantallazo tomado el %s',
+ 'Add a screenshot' => 'Añadir un pantallazo',
+ '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 tu proyecto.',
+ 'Identifier' => 'Identificador',
+ 'Postmark (incoming emails)' => 'Matasellos (emails entrantes)',
+ 'Help on Postmark integration' => 'Ayuda sobre la integración de Matasellos',
+ 'Mailgun (incoming emails)' => 'Mailgun (emails entrantes)',
+ 'Help on Mailgun integration' => 'Ayuda sobre la integración con Mailgun',
+ 'Sendgrid (incoming emails)' => 'Sendgrid (emails entrantes)',
+ 'Help on Sendgrid integration' => 'Ayuda sobre la integración con Sendgrid',
+ '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 quieres desactuvar la autenticación de dos factores para este usuario: "%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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
new file mode 100644
index 00000000..2d1edbc7
--- /dev/null
+++ b/app/Locale/fi_FI/translations.php
@@ -0,0 +1,925 @@
+<?php
+
+return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
+ 'None' => 'Ei mikään',
+ 'edit' => 'muokkaa',
+ 'Edit' => 'Muokkaa',
+ 'remove' => 'poista',
+ 'Remove' => 'Poista',
+ 'Update' => 'Päivitä',
+ 'Yes' => 'Kyllä',
+ 'No' => 'Ei',
+ 'cancel' => 'peruuta',
+ 'or' => 'tai',
+ 'Yellow' => 'Keltainen',
+ 'Blue' => 'Sininen',
+ 'Green' => 'Vihreä',
+ 'Purple' => 'Violetti',
+ 'Red' => 'Punainen',
+ 'Orange' => 'Oranssi',
+ 'Grey' => 'Harmaa',
+ 'Save' => 'Tallenna',
+ 'Login' => 'Sisäänkirjautuminen',
+ 'Official website:' => 'Virallinen verkkosivu:',
+ 'Unassigned' => 'Ei suorittajaa',
+ 'View this task' => 'Näytä tämä tehtävä',
+ 'Remove user' => 'Poista käyttäjä',
+ 'Do you really want to remove this user: "%s"?' => 'Oletko varma että haluat poistaa käyttäjän "%s"?',
+ 'New user' => 'Uusi käyttäjä',
+ 'All users' => 'Kaikki käyttäjät',
+ 'Username' => 'Käyttäjänimi',
+ 'Password' => 'Salasana',
+ 'Default project' => 'Oletusprojekti',
+ 'Administrator' => 'Ylläpitäjä',
+ 'Sign in' => 'Kirjaudu sisään',
+ 'Users' => 'Käyttäjät',
+ 'No user' => 'Ei käyttäjää',
+ 'Forbidden' => 'Estetty',
+ 'Access Forbidden' => 'Pääsy estetty',
+ 'Only administrators can access to this page.' => 'Vain ylläpitäjillä on pääsy tälle sivulle.',
+ 'Edit user' => 'Muokkaa käyttäjää',
+ 'Logout' => 'Kirjaudu ulos',
+ 'Bad username or password' => 'Väärä käyttäjätunnus tai salasana',
+ 'users' => 'käyttäjät',
+ 'projects' => 'projektit',
+ 'Edit project' => 'Muokkaa projektia',
+ 'Name' => 'Nimi',
+ 'Activated' => 'Aktivoitu',
+ 'Projects' => 'Projektit',
+ 'No project' => 'Ei projektia',
+ 'Project' => 'Projekti',
+ 'Status' => 'Status',
+ 'Tasks' => 'Tehtävät',
+ 'Board' => 'Taulu',
+ 'Actions' => 'Toiminnot',
+ 'Inactive' => 'Ei aktiivinen',
+ 'Active' => 'Aktiivinen',
+ 'Column %d' => 'Sarake %d',
+ 'Add this column' => 'Lisää tämä sarake',
+ '%d tasks on the board' => '%d tehtävää taululla',
+ '%d tasks in total' => '%d tehtävää yhteensä',
+ 'Unable to update this board.' => 'Taulun muuttaminen ei onnistunut.',
+ 'Edit board' => 'Muuta taulua',
+ 'Disable' => 'Disabloi',
+ 'Enable' => 'Aktivoi',
+ 'New project' => 'Uusi projekti',
+ 'Do you really want to remove this project: "%s"?' => 'Haluatko varmasti poistaa projektin: "%s"?',
+ 'Remove project' => 'Poista projekti',
+ 'Boards' => 'Taulut',
+ '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',
+ 'Add Column' => 'Lisää sarake',
+ 'Project "%s"' => 'Projekti "%s"',
+ 'Nobody assigned' => 'Ei suorittajaa',
+ 'Assigned to %s' => 'Tekijä: %s',
+ 'Remove a column' => 'Poista sarake',
+ 'Remove a column from a board' => 'Poista sarake taulusta',
+ 'Unable to remove this column.' => 'Sarakkeen poistaminen ei onnistunut.',
+ 'Do you really want to remove this column: "%s"?' => 'Haluatko varmasti poistaa sarakkeen "%s"?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Tämä toiminto POISTAA KAIKKI TEHTÄVÄT tästä sarakkeesta!',
+ 'Settings' => 'Asetukset',
+ 'Application settings' => 'Ohjelman asetukset',
+ 'Language' => 'Kieli',
+ 'Webhook token:' => 'Webhooks avain:',
+ // 'API token:' => '',
+ 'More information' => 'Lisätietoja',
+ 'Database size:' => 'Tietokannan koko:',
+ 'Download the database' => 'Lataa tietokanta',
+ 'Optimize the database' => 'Optimoi tietokanta',
+ '(VACUUM command)' => '(VACUUM-komento)',
+ '(Gzip compressed Sqlite file)' => '(Gzip-pakattu Sqlite-tiedosto)',
+ 'User settings' => 'Käyttäjän asetukset',
+ 'My default project:' => 'Oletusprojektini: ',
+ 'Close a task' => 'Sulje tehtävä',
+ 'Do you really want to close this task: "%s"?' => 'Haluatko varmasti sulkea tehtävän: "%s"?',
+ 'Edit a task' => 'Muokkaa tehtävää',
+ 'Column' => 'Sarake',
+ 'Color' => 'Väri',
+ 'Assignee' => 'Suorittaja',
+ 'Create another task' => 'Luo toinen tehtävä',
+ 'New task' => 'Uusi tehtävä',
+ '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.',
+ 'Add a new task' => 'Lisää uusi tehtävä',
+ 'The username is required' => 'Käyttäjätunnut vaaditaan',
+ 'The maximum length is %d characters' => 'Maksimipituus on %d merkkiä',
+ 'The minimum length is %d characters' => 'Vähimmäispituus on %d merkkiä',
+ 'The password is required' => 'Salasana vaaditaan',
+ 'This value must be an integer' => 'Tämän arvon täytyy olla numero',
+ 'The username must be unique' => 'Käyttäjänimi täytyy olla uniikki',
+ 'The username must be alphanumeric' => 'Käyttäjänimen täytyy olla alfanumeerinen',
+ 'The user id is required' => 'Käyttäjän id on pakollinen',
+ // 'Passwords don\'t match' => '',
+ 'The confirmation is required' => 'Varmistus vaaditaan',
+ 'The column is required' => 'Sarake on pakollinen',
+ 'The project is required' => 'Projekti on pakollinen',
+ 'The color is required' => 'Väri on pakollinen',
+ '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',
+ 'The language is required' => 'Kieli on pakollinen',
+ 'There is no active project, the first step is to create a new project.' => 'Aktiivista projektia ei ole, ensimmäinen vaihe on luoda uusi projekti.',
+ 'Settings saved successfully.' => 'Asetukset tallennettu onnistuneesti.',
+ 'Unable to save your settings.' => 'Asetusten tallentaminen epäonnistui.',
+ 'Database optimization done.' => 'Tietokannan optimointi suoritettu.',
+ 'Your project have been created successfully.' => 'Projekti luotiin onnistuneesti.',
+ 'Unable to create your project.' => 'Projektin luominen epäonnistui.',
+ 'Project updated successfully.' => 'Projekti päivitettiin onnistuneesti.',
+ 'Unable to update this project.' => 'Projektin muuttaminen epäonnistui.',
+ 'Unable to remove this project.' => 'Projektin poistaminen epäonnistui.',
+ 'Project removed successfully.' => 'Projekti poistettiin onnistuneesti.',
+ 'Project activated successfully.' => 'Projekti aktivoitiin onnistuneesti.',
+ 'Unable to activate this project.' => 'Projektin aktivoiminen epäonnistui.',
+ 'Project disabled successfully.' => 'Projektin disabloiminen onnistui.',
+ 'Unable to disable this project.' => 'Projektin disabloiminen epäonnistui.',
+ 'Unable to open this task.' => 'Tehtävän avaus epäonnistui.',
+ 'Task opened successfully.' => 'Tehtävä avattiin onnistuneesti.',
+ 'Unable to close this task.' => 'Tehtävän sulkeminen epäonnistui.',
+ 'Task closed successfully.' => 'Tehtävä suljettiin onnistuneesti.',
+ 'Unable to update your task.' => 'Tehtävän muokkaaminen epäonnistui.',
+ 'Task updated successfully.' => 'Tehtävä päivitettiin onnistuneesti.',
+ 'Unable to create your task.' => 'Tehtävän luominen epäonnistui.',
+ 'Task created successfully.' => 'Tehtävä luotiin onnistuneesti.',
+ 'User created successfully.' => 'Käyttäjä lisättiin onnistuneesti.',
+ 'Unable to create your user.' => 'Käyttäjän lisäys epäonnistui.',
+ 'User updated successfully.' => 'Käyttäjätietojen päivitys onnistui.',
+ 'Unable to update your user.' => 'Käyttäjätietojen päivitys epäonnistui.',
+ 'User removed successfully.' => 'Käyttäjä poistettiin onnistuneesti.',
+ 'Unable to remove this user.' => 'Käyttäjän poistaminen epäonnistui.',
+ 'Board updated successfully.' => 'Taulu päivitettiin onnistuneesti.',
+ 'Ready' => 'Valmis',
+ 'Backlog' => 'Tehtäväjono',
+ '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',
+ 'No task' => 'Ei tehtävää',
+ 'Completed tasks' => 'Valmiit tehtävät',
+ 'List of projects' => 'Projektit',
+ 'Completed tasks for "%s"' => 'Suoritetut tehtävät projektille %s',
+ '%d closed tasks' => '%d suljettua tehtävää',
+ 'No task for this project' => 'Ei tehtävää tälle projektille',
+ 'Public link' => 'Julkinen linkki',
+ 'There is no column in your project!' => 'Projektilta puuttuu sarakkeet!',
+ 'Change assignee' => 'Vaihda suorittajaa',
+ 'Change assignee for the task "%s"' => 'Vaihda suorittajaa tehtävälle %s',
+ 'Timezone' => 'Aikavyöhyke',
+ 'Sorry, I didn\'t find this information in my database!' => 'Anteeksi, en löytänyt tätä tietoa tietokannastani',
+ 'Page not found' => 'Sivua ei löydy',
+ 'Complexity' => 'Monimutkaisuus',
+ 'limit' => 'raja',
+ 'Task limit' => 'Tehtävien maksimimäärä',
+ 'Task count' => 'Tehtävien määrä',
+ 'This value must be greater than %d' => 'Arvon täytyy olla suurempi kuin %d',
+ 'Edit project access list' => 'Muuta projektin käyttäjiä',
+ 'Edit users access' => 'Muuta käyttäjien pääsyä',
+ 'Allow this user' => 'Salli tämä projekti',
+ 'Only those users have access to this project:' => 'Vain näillä käyttäjillä on pääsy projektiin:',
+ '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.' => '',
+ 'You are not allowed to access to this project.' => 'Sinulla ei ole pääsyä tähän projektiin.',
+ 'Comments' => 'Kommentit',
+ 'Post comment' => 'Lisää kommentti',
+ 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla',
+ 'Leave a comment' => 'Lisää kommentti',
+ 'Comment is required' => 'Kommentti vaaditaan',
+ 'Leave a description' => 'Lisää kuvaus',
+ 'Comment added successfully.' => 'Kommentti lisättiin onnistuneesti.',
+ 'Unable to create your comment.' => 'Kommentin lisäys epäonnistui.',
+ 'The description is required' => 'Kuvaus vaaditaan',
+ '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.',
+ 'Remove an action' => 'Poista toiminto',
+ 'Unable to remove this action.' => 'Toiminnon poistaminen epäonnistui.',
+ 'Action removed successfully.' => 'Toiminto poistettiin onnistuneesti.',
+ 'Automatic actions for the project "%s"' => 'Automaattiset toiminnot projektille "%s"',
+ 'Defined actions' => 'Määritellyt toiminnot',
+ // 'Add an action' => '',
+ 'Event name' => 'Tapahtuman nimi',
+ 'Action name' => 'Toiminnon nimi',
+ 'Action parameters' => 'Toiminnon parametrit',
+ 'Action' => 'Toiminto',
+ 'Event' => 'Tapahtuma',
+ 'When the selected event occurs execute the corresponding action.' => 'Kun valittu tapahtuma tapahtuu, suorita vastaava toiminto.',
+ 'Next step' => 'Seuraava vaihe',
+ 'Define action parameters' => 'Määrittele toiminnon parametrit',
+ 'Save this action' => 'Tallenna toiminto',
+ 'Do you really want to remove this action: "%s"?' => 'Oletko varma että haluat poistaa toiminnon "%s"?',
+ 'Remove an automatic action' => 'Poista automaattintn toiminto',
+ 'Close the task' => 'Sulje tehtävä',
+ 'Assign the task to a specific user' => 'Osoita tehtävä käyttäjälle',
+ 'Assign the task to the person who does the action' => 'Määritä suorittaja tehtävälle',
+ 'Duplicate the task to another project' => 'Monista tehtävä toiselle projektille',
+ 'Move a task to another column' => 'Siirrä tehtävä toiseen sarakkeeseen',
+ 'Move a task to another position in the same column' => 'Siirrä tehtävä eri järjestykseen samassa sarakkeessa',
+ 'Task modification' => 'Tehtävän muokkaus',
+ 'Task creation' => 'Tehtävän luominen',
+ 'Open a closed task' => 'Avaa jo suljettu tehtävä',
+ 'Closing a task' => 'Tehtävää suljetaan',
+ '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',
+ 'Update this comment' => 'Muuta projektia',
+ 'Comment updated successfully.' => 'Kommentti päivitettiin onnistuneesti.',
+ 'Unable to update your comment.' => 'Kommentin päivitys epäonnistui.',
+ 'Remove a comment' => 'Poista kommentti',
+ '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.',
+ 'Details' => 'Tiedot',
+ 'Current password for the user "%s"' => 'Käyttäjän "%s" salasana',
+ 'The current password is required' => 'Salasana vaaditaan',
+ 'Wrong password' => 'Väärä salasana',
+ 'Reset all tokens' => 'Resetoi kaikki tokenit',
+ 'All tokens have been regenerated.' => 'Kaikki tokenit luotiin uudelleen.',
+ 'Unknown' => 'Tuntematon',
+ 'Last logins' => 'Viimeisimmät kirjautumiset',
+ 'Login date' => 'Kirjautumispäivä',
+ 'Authentication method' => 'Autentikointimenetelmä',
+ 'IP address' => 'IP-Osoite',
+ 'User agent' => 'Selain',
+ 'Persistent connections' => 'Voimassa olevat yhteydet',
+ 'No session.' => 'Ei sessioita.',
+ 'Expiration date' => 'Vanhentumispäivä',
+ 'Remember Me' => 'Muista minut',
+ 'Creation date' => 'Luomispäivä',
+ 'Filter by user' => 'Rajaa käyttäjän mukaan',
+ 'Filter by due date' => 'Rajaa deadlinen mukaan',
+ 'Everybody' => 'Kaikki',
+ 'Open' => 'Avoin',
+ 'Closed' => 'Suljettu',
+ 'Search' => 'Etsi',
+ 'Nothing found.' => 'Ei löytynyt.',
+ 'Search in the project "%s"' => 'Etsi projektista "%s"',
+ 'Due date' => 'Deadline',
+ 'Others formats accepted: %s and %s' => 'Muut hyväksytyt muodot: %s ja %s',
+ 'Description' => 'Kuvaus',
+ '%d comments' => '%d kommenttia',
+ '%d comment' => '%d kommentti',
+ 'Email address invalid' => 'Email ei kelpaa',
+ 'Your Google Account is not linked anymore to your profile.' => 'Google tunnustasi ei ole enää linkattu profiiliisi',
+ 'Unable to unlink your Google Account.' => 'Google tunnuksen linkkaamisen poistaminen epäonnistui.',
+ 'Google authentication failed' => 'Google autentikointi epäonnistui',
+ 'Unable to link your Google Account.' => 'Google tunnuksen linkkaaminen epäonnistui.',
+ 'Your Google Account is linked to your profile successfully.' => 'Google tunnuksesi linkitettiin profiiliisi onnistuneesti.',
+ '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 #%d' => 'Tehtävä #%d',
+ 'Task removed successfully.' => 'Tehtävä poistettiin onnistuneesti.',
+ 'Unable to remove this task.' => 'Tehtävän poistaminen epäonnistui.',
+ 'Remove a task' => 'Poista tehtävä',
+ 'Do you really want to remove this task: "%s"?' => 'Haluatko varmasti poistaa tehtävän: "%s"?',
+ 'Assign automatically a color based on a category' => 'Aseta väri automaattisesti kategorian mukaan',
+ 'Assign automatically a category based on a color' => 'Aseta kategoria automaattisesti värin mukaan',
+ 'Task creation or modification' => 'Tehtävän luonti tai muuttaminen',
+ 'Category' => 'Kategoria',
+ 'Category:' => 'Kategoria:',
+ 'Categories' => 'Kategoriat',
+ 'Category not found.' => 'Kategoriaa ei löytynyt.',
+ 'Your category have been created successfully.' => 'Kategoria luotiin onnistuneesti.',
+ 'Unable to create your category.' => 'Kategorian luonti epäonnistui.',
+ 'Your category have been updated successfully.' => 'Kategoriaa muokattiin onnistuneesti.',
+ 'Unable to update your category.' => 'Kategorian muokkaaminen epäonnistui.',
+ 'Remove a category' => 'Poista kategoria',
+ 'Category removed successfully.' => 'Kategoria poistettu onnistuneesti.',
+ 'Unable to remove this category.' => 'Kategorian poisto epäonnistui.',
+ 'Category modification for the project "%s"' => 'Kategorian muutos projektissa "%s"',
+ 'Category Name' => 'Kategorian nimi',
+ 'Categories for the project "%s"' => 'Kategoriat projektille "%s"',
+ 'Add a new category' => 'Lisää uusi kategoria',
+ 'Do you really want to remove this category: "%s"?' => 'Haluatko varmasti poistaa kategorian: "%s"?',
+ 'Filter by category' => 'Rajaa kategorian mukaan',
+ 'All categories' => 'Kaikki kategoriat',
+ 'No category' => 'Kategoriaa ei löydy',
+ 'The name is required' => 'Nimi vaaditaan',
+ 'Remove a file' => 'Poista tiedosto',
+ 'Unable to remove this file.' => 'Tiedoston poistaminen epäonnistui.',
+ 'File removed successfully.' => 'Tiedosto poistettiin onnistuneesti.',
+ 'Attach a document' => 'Liitä dokumentti',
+ 'Do you really want to remove this file: "%s"?' => 'Haluatko varmasti poistaa tiedoston: "%s"?',
+ 'open' => 'avaa',
+ 'Attachments' => 'Liitteet',
+ 'Edit the task' => 'Muokkaa tehtävää',
+ 'Edit the description' => 'Muokkaa kuvausta',
+ 'Add a comment' => 'Lisää kommentti',
+ 'Edit a comment' => 'Muokkaa kommenttia',
+ 'Summary' => 'Yhteenveto',
+ 'Time tracking' => 'Ajan seuranta',
+ 'Estimate:' => 'Arvio:',
+ 'Spent:' => 'Käytetty:',
+ 'Do you really want to remove this sub-task?' => 'Haluatko varmasti poistaa tämän alitehtävän?',
+ 'Remaining:' => 'Jäljellä',
+ 'hours' => 'tuntia',
+ 'spent' => 'käytetty',
+ 'estimated' => 'estimoitu',
+ 'Sub-Tasks' => 'Alitehtävät',
+ 'Add a sub-task' => 'Lisää alitehtävä',
+ 'Original estimate' => 'Alkuperäinen estimaatti',
+ 'Create another sub-task' => 'Lisää toinen alitehtävä',
+ 'Time spent' => 'Käytetty aika',
+ 'Edit a sub-task' => 'Muokkaa alitehtävää',
+ 'Remove a sub-task' => 'Poista alitehtävä',
+ 'The time must be a numeric value' => 'Ajan pitää olla numero',
+ 'Todo' => 'Todo',
+ 'In progress' => 'Työnalla',
+ 'Sub-task removed successfully.' => 'Alitehtävä poistettu onnistuneesti.',
+ 'Unable to remove this sub-task.' => 'Alitehtävän poistaminen epäonnistui.',
+ 'Sub-task updated successfully.' => 'Alitehtävä päivitettiin onnistuneesti.',
+ 'Unable to update your sub-task.' => 'Alitehtävän päivitys epäonnistui.',
+ 'Unable to create your sub-task.' => 'Alitehtävän luonti epäonnistui.',
+ 'Sub-task added successfully.' => 'Alitehtävä luotiin onnistuneesti.',
+ 'Maximum size: ' => 'Maksimikoko: ',
+ 'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.',
+ 'Display another project' => 'Näytä toinen projekti',
+ 'Your GitHub account was successfully linked to your profile.' => 'Github-tilisi on onnistuneesti liitetty profiiliisi',
+ 'Unable to link your GitHub Account.' => 'Github-tilin liittäminen epäonnistui',
+ 'GitHub authentication failed' => 'Github-todennus epäonnistui',
+ 'Your GitHub account is no longer linked to your profile.' => 'Github-tiliäsi ei ole enää liitetty profiiliisi.',
+ 'Unable to unlink your GitHub Account.' => 'Github-tilisi liitoksen poisto epäonnistui',
+ '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ä',
+ 'End Date' => 'Lopetuspäivä',
+ 'Execute' => 'Suorita',
+ 'Task Id' => 'Tehtävän ID',
+ 'Creator' => 'Luonut',
+ 'Modification date' => 'Muokkauspäivä',
+ 'Completion date' => 'Valmistumispäivä',
+ 'Clone' => 'Kahdenna',
+ 'Clone Project' => 'Kahdenna projekti',
+ 'Project cloned successfully.' => 'Projekti kahdennettu onnistuneesti',
+ 'Unable to clone this project.' => 'Projektin kahdennus epäonnistui',
+ 'Email notifications' => 'Sähköposti-ilmoitukset',
+ 'Enable email notifications' => 'Ota käyttöön sähköposti-ilmoitukset',
+ 'Task position:' => 'Tehtävän sijainti',
+ 'The task #%d have been opened.' => 'Tehtävä #%d on avattu',
+ 'The task #%d have been closed.' => 'Tehtävä #%d on suljettu',
+ 'Sub-task updated' => 'Alitehtävä päivitetty',
+ 'Title:' => 'Otsikko:',
+ 'Status:' => 'Tila:',
+ 'Assignee:' => 'Vastaanottaja:',
+ 'Time tracking:' => 'Ajan seuranta:',
+ 'New sub-task' => 'Uusi alitehtävä',
+ 'New attachment added "%s"' => 'Uusi liite lisätty "%s"',
+ 'Comment updated' => 'Kommentti päivitetty',
+ 'New comment posted by %s' => '%s lisäsi uuden kommentin',
+ // 'List of due tasks for the project "%s"' => '',
+ // 'New attachment' => '',
+ // 'New comment' => '',
+ // 'New subtask' => '',
+ // 'Subtask updated' => '',
+ // 'Task updated' => '',
+ // 'Task closed' => '',
+ // 'Task opened' => '',
+ // '[%s][Due tasks]' => '',
+ // '[Kanboard] Notification' => '',
+ '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',
+ 'Category management' => 'Kategorioiden hallinta',
+ '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 ',
+ 'Active projects' => 'Aktiiviset projektit',
+ 'Inactive projects' => 'Passiiviset projektit',
+ 'Public access disabled' => 'Julkinen käyttöoikeus ei ole käytössä',
+ 'Do you really want to disable this project: "%s"?' => 'Haluatko varmasti tehdä projektista "%s" passiivisen?',
+ 'Do you really want to duplicate this project: "%s"?' => 'Haluatko varmasti kahdentaa projektin "%s"?',
+ 'Do you really want to enable this project: "%s"?' => 'Haluatko varmasti aktivoida projektinen "%s"',
+ 'Project activation' => 'Projektin aktivointi',
+ 'Move the task to another project' => 'Siirrä tehtävä toiseen projektiin',
+ 'Move to another project' => 'Siirrä toiseen projektiin',
+ 'Do you really want to duplicate this task?' => 'Haluatko varmasti kahdentaa tämän tehtävän?',
+ 'Duplicate a task' => 'Kahdenna tehtävä',
+ 'External accounts' => 'Muut tilit',
+ 'Account type' => 'Tilin tyyppi',
+ 'Local' => 'Paikallinen',
+ 'Remote' => 'Etä',
+ 'Enabled' => 'Käytössä',
+ 'Disabled' => 'Pois käytöstä',
+ 'Google account linked' => 'Google-tili liitetty',
+ 'Github account linked' => 'Github-tili liitetty',
+ 'Username:' => 'Käyttäjänimi:',
+ 'Name:' => 'Nimi:',
+ 'Email:' => 'Sähköpostiosoite:',
+ 'Default project:' => 'Oletusprojekti:',
+ '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.',
+ 'Change category for the task "%s"' => 'Vaihda tehtävän "%s" kategoria',
+ 'Change category' => 'Vaihda kategoria',
+ '%s updated the task %s' => '%s päivitti tehtävän %s',
+ '%s opened the task %s' => '%s avasi tehtävän %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s siirsi tehtävän %s %d. sarakkeessa "%s"',
+ '%s moved the task %s to the column "%s"' => '%s siirsi tehtävän %s sarakkeeseen "%s"',
+ '%s created the task %s' => '%s loi tehtävän %s',
+ '%s closed the task %s' => '%s sulki tehtävän %s',
+ '%s created a subtask for the task %s' => '%s loi alitehtävän tehtävälle %s',
+ '%s updated a subtask for the task %s' => '%s päivitti tehtävän %s alitehtävää',
+ 'Assigned to %s with an estimate of %s/%sh' => 'Annettu henkilölle %s arviolla %s/%sh',
+ 'Not assigned, estimate of %sh' => 'Ei annettu kenellekään, arvio %sh',
+ '%s updated a comment on the task %s' => '%s päivitti kommentia tehtävässä %s',
+ '%s commented the task %s' => '%s kommentoi tehtävää %s',
+ '%s\'s activity' => 'Henkilön %s toiminta',
+ 'No activity.' => 'Ei toimintaa.',
+ 'RSS feed' => 'RSS-syöte',
+ '%s updated a comment on the task #%d' => '%s päivitti kommenttia tehtävässä #%d',
+ '%s commented on the task #%d' => '%s kommentoi tehtävää #%d',
+ '%s updated a subtask for the task #%d' => '%s päivitti tehtävän #%d alitehtävää',
+ '%s created a subtask for the task #%d' => '%s loi alitehtävän tehtävälle #%d',
+ '%s updated the task #%d' => '%s päivitti tehtävää #%d',
+ '%s created the task #%d' => '%s loi tehtävän #%d',
+ '%s closed the task #%d' => '%s sulki tehtävän #%d',
+ '%s open the task #%d' => '%s avasi tehtävän #%d',
+ '%s moved the task #%d to the column "%s"' => '%s siirsi tehtävän #%d sarakkeeseen "%s"',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s siirsi tehtävän #%d %d. sarakkeessa %s',
+ 'Activity' => 'Toiminta',
+ 'Default values are "%s"' => 'Oletusarvot ovat "%s"',
+ 'Default columns for new projects (Comma-separated)' => 'Oletussarakkeet uusille projekteille',
+ 'Task assignee change' => 'Tehtävän saajan vaihto',
+ '%s change the assignee of the task #%d to %s' => '%s vaihtoi tehtävän #%d saajaksi %s',
+ '%s changed the assignee of the task %s to %s' => '%s vaihtoi tehtävän %s saajaksi %s',
+ // 'Column Change' => '',
+ // 'Position Change' => '',
+ // 'Assignee Change' => '',
+ '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',
+ 'Database driver:' => 'Tietokantaohjelmisto:',
+ 'Board settings' => 'Taulun asetukset',
+ 'URL and token' => 'URL ja token',
+ 'Webhook settings' => 'Webhookin asetukset',
+ 'URL for task creation:' => 'URL tehtävän luomiseksi:',
+ 'Reset token' => 'Vaihda token',
+ 'API endpoint:' => 'API päätepiste:',
+ 'Refresh interval for private board' => 'Päivitystiheys yksityisille tauluille',
+ 'Refresh interval for public board' => 'Päivitystiheys julkisille tauluille',
+ 'Task highlight period' => 'Tehtävän korostusaika',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Aika (sekunteina) kuinka kauan tehtävä voidaan katsoa äskettäin muokatuksi (0 poistaa toiminnon käytöstä, oletuksena 2 päivää)',
+ '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',
+ 'New private project' => 'Uusi yksityinen projekti',
+ 'This project is private' => 'Tämä projekti on yksityinen',
+ 'Type here to create a new sub-task' => 'Kirjoita tähän luodaksesi uuden alitehtävän',
+ 'Add' => 'Lisää',
+ '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.',
+ 'My tasks' => 'Minun tehtävät',
+ 'Activity stream' => 'Toiminta',
+ 'Dashboard' => 'Työpöytä',
+ 'Confirmation' => 'Vahvistus',
+ 'Allow everybody to access to this project' => 'Anna kaikille käyttöoikeus tähän projektiin',
+ 'Everybody have access to this project.' => 'Kaikilla on käyttöoikeus projektiin.',
+ // 'Webhooks' => '',
+ // 'API' => '',
+ 'Integration' => 'Integraatio',
+ // 'Github webhooks' => '',
+ // 'Help on Github webhooks' => '',
+ // 'Create a comment from an external provider' => '',
+ // 'Github issue comment created' => '',
+ 'Configure' => 'Konfiguroi',
+ 'Project management' => 'Projektin hallinta',
+ 'My projects' => 'Minun projektini',
+ 'Columns' => 'Sarakkeet',
+ 'Task' => 'Tehtävät',
+ 'Your are not member of any project.' => 'Et ole minkään projektin jäsen.',
+ 'Percentage' => 'Prosentti',
+ 'Number of tasks' => 'Tehtävien määrä',
+ 'Task distribution' => 'Tehtävien jakauma',
+ 'Reportings' => 'Raportoinnit',
+ // 'Task repartition for "%s"' => '',
+ 'Analytics' => 'Analytiikka',
+ 'Subtask' => 'Alitehtävä',
+ 'My subtasks' => 'Minun alitehtäväni',
+ // 'User repartition' => '',
+ // 'User repartition for "%s"' => '',
+ 'Clone this project' => 'Kahdenna projekti',
+ 'Column removed successfully.' => 'Sarake poistettu onnstuneesti.',
+ 'Edit Project' => 'Muokkaa projektia',
+ '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',
+ 'The project id must be an integer' => 'Projektin ID:n on oltava kokonaisluku',
+ 'The status must be an integer' => 'Tilan on oltava kokonaisluku',
+ 'The subtask id is required' => 'Alitehtävän ID vaaditaan',
+ 'The subtask id must be an integer' => 'Alitehtävän ID:ntulee olla kokonaisluku',
+ 'The task id is required' => 'Tehtävän ID vaaditaan',
+ 'The task id must be an integer' => 'Tehtävän ID on oltava kokonaisluku',
+ 'The user id must be an integer' => 'Käyttäjän ID on oltava kokonaisluku',
+ 'This value is required' => 'Tämä arvo on pakollinen',
+ 'This value must be numeric' => 'Tämän arvon tulee olla numeerinen',
+ 'Unable to create this task.' => 'Tehtävän luonti epäonnistui',
+ 'Cumulative flow diagram' => 'Kumulatiivinen vuokaavio',
+ 'Cumulative flow diagram for "%s"' => 'Kumulatiivinen vuokaavio kohteelle "%s"',
+ 'Daily project summary' => 'Päivittäinen yhteenveto',
+ 'Daily project summary export' => 'Päivittäisen yhteenvedon vienti',
+ 'Daily project summary export for "%s"' => 'Päivittäisen yhteenvedon vienti kohteeseen "%s"',
+ 'Exports' => 'Viennit',
+ 'This export contains the number of tasks per column grouped per day.' => 'Tämä tiedosto sisältää tehtäviä sarakkeisiin päiväkohtaisesti ryhmilteltyinä',
+ 'Nothing to preview...' => 'Ei esikatselua...',
+ 'Preview' => 'Ei esikatselua',
+ 'Write' => 'Kirjoita',
+ 'Active swimlanes' => 'Aktiiviset kaistat',
+ 'Add a new swimlane' => 'Lisää uusi kaista',
+ 'Change default swimlane' => 'Vaihda oletuskaistaa',
+ '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',
+ 'Swimlane removed successfully.' => 'Kaista poistettu onnistuneesti.',
+ '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' => '',
+ // 'Subtasks exportation for "%s"' => '',
+ // 'Task Title' => '',
+ // 'Untitled' => '',
+ // 'Application default' => '',
+ // 'Language:' => '',
+ // 'Timezone:' => '',
+ // 'All columns' => '',
+ // 'Calendar for "%s"' => '',
+ // 'Filter by column' => '',
+ // 'Filter by status' => '',
+ // 'Calendar' => '',
+ // 'Next' => '',
+ // '#%d' => '',
+ // 'Filter by color' => '',
+ // 'Filter by swimlane' => '',
+ // 'All swimlanes' => '',
+ // 'All colors' => '',
+ // 'All status' => '',
+ // 'Add a comment logging moving the task between columns' => '',
+ // 'Moved to column %s' => '',
+ // 'Change description' => '',
+ // 'User dashboard' => '',
+ // 'Allow only one subtask in progress at the same time for a user' => '',
+ // 'Edit column "%s"' => '',
+ // 'Enable time tracking for subtasks' => '',
+ // 'Select the new status of the subtask: "%s"' => '',
+ // 'Subtask timesheet' => '',
+ // 'There is nothing to show.' => '',
+ // 'Time Tracking' => '',
+ // 'You already have one subtask in progress' => '',
+ // 'Which parts of the project do you want to duplicate?' => '',
+ // 'Change dashboard view' => '',
+ // 'Show/hide activities' => '',
+ // 'Show/hide projects' => '',
+ // 'Show/hide subtasks' => '',
+ // 'Show/hide tasks' => '',
+ // 'Disable login form' => '',
+ // 'Show/hide calendar' => '',
+ // 'User calendar' => '',
+ // 'Bitbucket commit received' => '',
+ // 'Bitbucket webhooks' => '',
+ // 'Help on Bitbucket webhooks' => '',
+ // 'Start' => '',
+ // 'End' => '',
+ // 'Task age in days' => '',
+ // 'Days in this column' => '',
+ // '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
+ // 'Filter recently updated' => '',
+ // 'since %B %e, %Y at %k:%M %p' => '',
+ // 'More filters' => '',
+ // 'Compact view' => '',
+ // 'Horizontal scrolling' => '',
+ // 'Compact/wide view' => '',
+ // 'No results match:' => '',
+ // 'Remove hourly rate' => '',
+ // 'Do you really want to remove this hourly rate?' => '',
+ // 'Hourly rates' => '',
+ // 'Hourly rate' => '',
+ // 'Currency' => '',
+ // 'Effective date' => '',
+ // 'Add new rate' => '',
+ // 'Rate removed successfully.' => '',
+ // 'Unable to remove this rate.' => '',
+ // 'Unable to save the hourly rate.' => '',
+ // 'Hourly rate created successfully.' => '',
+ // 'Start time' => '',
+ // 'End time' => '',
+ // 'Comment' => '',
+ // 'All day' => '',
+ // 'Day' => '',
+ // 'Manage timetable' => '',
+ // 'Overtime timetable' => '',
+ // 'Time off timetable' => '',
+ // 'Timetable' => '',
+ // 'Work timetable' => '',
+ // 'Week timetable' => '',
+ // 'Day timetable' => '',
+ // 'From' => '',
+ // 'To' => '',
+ // 'Time slot created successfully.' => '',
+ // 'Unable to save this time slot.' => '',
+ // 'Time slot removed successfully.' => '',
+ // 'Unable to remove this time slot.' => '',
+ // 'Do you really want to remove this time slot?' => '',
+ // 'Remove time slot' => '',
+ // 'Add new time slot' => '',
+ // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
+ // 'Files' => '',
+ // 'Images' => '',
+ // 'Private project' => '',
+ // 'Amount' => '',
+ // 'AUD - Australian Dollar' => '',
+ // 'Budget' => '',
+ // 'Budget line' => '',
+ // 'Budget line removed successfully.' => '',
+ // 'Budget lines' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ // 'Cost' => '',
+ // 'Cost breakdown' => '',
+ // 'Custom Stylesheet' => '',
+ // 'download' => '',
+ // 'Do you really want to remove this budget line?' => '',
+ // 'EUR - Euro' => '',
+ // 'Expenses' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'New budget line' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'Remove a budget line' => '',
+ // 'Remove budget line' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'The budget line have been created successfully.' => '',
+ // 'Unable to create the budget line.' => '',
+ // 'Unable to remove this budget line.' => '',
+ // 'USD - US Dollar' => '',
+ // 'Remaining' => '',
+ // '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' => '',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ // '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' => '',
+ // 'Currency rates are used to calculate project budget.' => '',
+ // 'Reference currency' => '',
+ // 'The currency rate have been added successfully.' => '',
+ // 'Unable to add this currency rate.' => '',
+ // 'Send notifications to a Slack channel' => '',
+ // 'Webhook URL' => '',
+ // 'Help on Slack integration' => '',
+ // '%s remove the assignee of the task %s' => '',
+ // 'Send notifications to Hipchat' => '',
+ // 'API URL' => '',
+ // 'Room API ID or name' => '',
+ // 'Room notification token' => '',
+ // 'Help on Hipchat integration' => '',
+ // '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.' => '',
+ // 'SEK - Swedish Krona' => '',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
+);
diff --git a/app/Locales/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index f4eaab11..f5c97759 100644
--- a/app/Locales/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -1,6 +1,8 @@
<?php
return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => ' ',
'None' => 'Aucun',
'edit' => 'modifier',
'Edit' => 'Modifier',
@@ -182,18 +184,19 @@ return array(
'Change assignee' => 'Changer la personne assignée',
'Change assignee for the task "%s"' => 'Changer la personne assignée pour la tâche « %s »',
'Timezone' => 'Fuseau horaire',
- 'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !',
+ 'Sorry, I didn\'t find this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !',
'Page not found' => 'Page introuvable',
'Complexity' => 'Complexité',
'limit' => 'limite',
- 'Task limit' => 'Nombre maximum de tâches',
+ 'Task limit' => 'Tâches Max.',
+ 'Task count' => 'Nombre de tâches',
'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d',
'Edit project access list' => 'Modifier l\'accès au projet',
'Edit users access' => 'Modifier les utilisateurs autorisés',
'Allow this user' => 'Autoriser cet utilisateur',
'Only those users have access to this project:' => 'Seulement ces utilisateurs ont accès à ce projet :',
'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.',
- 'revoke' => 'révoquer',
+ '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.',
@@ -212,6 +215,7 @@ return array(
'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é avec succès.',
'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.',
@@ -236,7 +240,7 @@ return array(
'Assign the task to a specific user' => 'Assigner la tâche à un utilisateur spécifique',
'Assign the task to the person who does the action' => 'Assigner la tâche à la personne qui fait l\'action',
'Duplicate the task to another project' => 'Dupliquer la tâche vers un autre projet',
- 'Move a task to another column' => 'Déplacement d\'une tâche vers un autre colonne',
+ 'Move a task to another column' => 'Déplacement d\'une tâche vers une autre colonne',
'Move a task to another position in the same column' => 'Déplacement d\'une tâche à une autre position mais dans la même colonne',
'Task modification' => 'Modification d\'une tâche',
'Task creation' => 'Création d\'une tâche',
@@ -276,7 +280,7 @@ return array(
'Remember Me' => 'Connexion automatique',
'Creation date' => 'Date de création',
'Filter by user' => 'Filtrer par utilisateur',
- 'Filter by due date' => 'Filtrer par date d\'échéance',
+ 'Filter by due date' => 'Avec une date d\'échéance',
'Everybody' => 'Tout le monde',
'Open' => 'Ouvert',
'Closed' => 'Fermé',
@@ -339,7 +343,7 @@ return array(
'Add a comment' => 'Ajouter un commentaire',
'Edit a comment' => 'Modifier un commentaire',
'Summary' => 'Résumé',
- 'Time tracking' => 'Gestion du temps',
+ 'Time tracking' => 'Suivi du temps',
'Estimate:' => 'Estimation :',
'Spent:' => 'Passé :',
'Do you really want to remove this sub-task?' => 'Voulez-vous vraiment supprimer cette sous-tâche ?',
@@ -385,8 +389,6 @@ return array(
'Creator' => 'Créateur',
'Modification date' => 'Date de modification',
'Completion date' => 'Date de complétion',
- 'Webhook URL for task creation' => 'URL du webhook pour la création de tâche',
- 'Webhook URL for task modification' => 'URL du webhook pour la modification de tâche',
'Clone' => 'Clone',
'Clone Project' => 'Cloner le projet',
'Project cloned successfully.' => 'Projet cloné avec succès.',
@@ -406,15 +408,15 @@ return array(
'Comment updated' => 'Commentaire ajouté',
'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »',
'List of due tasks for the project "%s"' => 'Liste des tâches expirées pour le projet « %s »',
- '[%s][New attachment] %s (#%d)' => '[%s][Pièce-jointe] %s (#%d)',
- '[%s][New comment] %s (#%d)' => '[%s][Nouveau commentaire] %s (#%d)',
- '[%s][Comment updated] %s (#%d)' => '[%s][Commentaire mis à jour] %s (#%d)',
- '[%s][New subtask] %s (#%d)' => '[%s][Nouvelle sous-tâche] %s (#%d)',
- '[%s][Subtask updated] %s (#%d)' => '[%s][Sous-tâche mise à jour] %s (#%d)',
- '[%s][New task] %s (#%d)' => '[%s][Nouvelle tâche] %s (#%d)',
- '[%s][Task updated] %s (#%d)' => '[%s][Tâche mise à jour] %s (#%d)',
- '[%s][Task closed] %s (#%d)' => '[%s][Tâche fermée] %s (#%d)',
- '[%s][Task opened] %s (#%d)' => '[%s][Tâche ouverte] %s (#%d)',
+ 'New attachment' => 'Nouveau document',
+ 'New comment' => 'Nouveau commentaire',
+ 'Comment updated' => 'Commentaire mis à jour',
+ 'New subtask' => 'Nouvelle sous-tâche',
+ 'Subtask updated' => 'Sous-tâche mise à jour',
+ 'New task' => 'Nouvelle tâche',
+ 'Task updated' => 'Tâche mise à jour',
+ 'Task closed' => 'Tâche fermée',
+ 'Task opened' => 'Tâche ouverte',
'[%s][Due tasks]' => '[%s][Tâches expirées]',
'[Kanboard] Notification' => '[Kanboard] Notification',
'I want to receive notifications only for those projects:' => 'Je souhaite reçevoir les notifications uniquement pour les projets sélectionnés :',
@@ -456,7 +458,7 @@ return array(
'Edit profile' => 'Modifier le profil',
'Change password' => 'Changer le mot de passe',
'Password modification' => 'Changement de mot de passe',
- 'External authentications' => 'Authentifications externe',
+ 'External authentications' => 'Authentifications externes',
'Google Account' => 'Compte Google',
'Github Account' => 'Compte Github',
'Never connected.' => 'Jamais connecté.',
@@ -467,18 +469,18 @@ return array(
'Unable to change the password.' => 'Impossible de changer le mot de passe.',
'Change category for the task "%s"' => 'Changer la catégorie pour la tâche « %s »',
'Change category' => 'Changer de catégorie',
- '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a mis à jour la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
- '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a ouvert la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s a déplacé la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a> à la position n°%d dans la colonne « %s »',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s a déplacé la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a> dans la colonne « %s »',
- '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a créé la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
- '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a fermé la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
- '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a créé une sous-tâche pour la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
- '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a mis à jour une sous-tâche appartenant à la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
+ '%s updated the task %s' => '%s a mis à jour la tâche %s',
+ '%s opened the task %s' => '%s a ouvert la tâche %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s a déplacé la tâche %s à la position n°%d dans la colonne « %s »',
+ '%s moved the task %s to the column "%s"' => '%s a déplacé la tâche %s dans la colonne « %s »',
+ '%s created the task %s' => '%s a créé la tâche %s',
+ '%s closed the task %s' => '%s a fermé la tâche %s',
+ '%s created a subtask for the task %s' => '%s a créé une sous-tâche pour la tâche %s',
+ '%s updated a subtask for the task %s' => '%s a mis à jour une sous-tâche appartenant à la tâche %s',
'Assigned to %s with an estimate of %s/%sh' => 'Assigné à %s avec un estimé de %s/%sh',
'Not assigned, estimate of %sh' => 'Personne assigné, estimé de %sh',
- '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a mis à jour un commentaire appartenant à la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
- '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s a ajouté un commentaire sur la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a>',
+ '%s updated a comment on the task %s' => '%s a mis à jour un commentaire appartenant à la tâche %s',
+ '%s commented the task %s' => '%s a ajouté un commentaire sur la tâche %s',
'%s\'s activity' => 'Activité du projet %s',
'No activity.' => 'Aucune activité.',
'RSS feed' => 'Flux RSS',
@@ -496,11 +498,11 @@ return array(
'Default values are "%s"' => 'Les valeurs par défaut sont « %s »',
'Default columns for new projects (Comma-separated)' => 'Colonnes par défaut pour les nouveaux projets (séparé par des virgules)',
'Task assignee change' => 'Modification de la personne assignée sur une tâche',
- '%s change the assignee of the task #%d to %s' => '%s a changé la personne assignée sur la tâche #%d pour %s',
- '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '%s a changé la personne assignée sur la tâche <a href="?controller=task&amp;action=show&amp;task_id=%d">n°%d</a> pour %s',
- '[%s][Column Change] %s (#%d)' => '[%s][Changement de colonne] %s (#%d)',
- '[%s][Position Change] %s (#%d)' => '[%s][Changement de position] %s (#%d)',
- '[%s][Assignee Change] %s (#%d)' => '[%s][Changement d\'assigné] %s (#%d)',
+ '%s change the assignee of the task #%d to %s' => '%s a changé la personne assignée sur la tâche n˚%d pour %s',
+ '%s changed the assignee of the task %s to %s' => '%s a changé la personne assignée sur la tâche %s pour %s',
+ 'Column Change' => 'Changement de colonne',
+ 'Position Change' => 'Changement de position',
+ 'Assignee Change' => 'Changement d\'assigné',
'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',
@@ -551,4 +553,375 @@ return array(
'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.',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Integration' => 'Intégration',
+ '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',
+ 'Configure' => 'Configurer',
+ 'Project management' => 'Gestion des projets',
+ 'My projects' => 'Mes projets',
+ 'Columns' => 'Colonnes',
+ 'Task' => 'Tâche',
+ 'Your are not member of any project.' => 'Vous n\'êtes membre d\'aucun projet.',
+ 'Percentage' => 'Pourcentage',
+ 'Number of tasks' => 'Nombre de tâches',
+ 'Task distribution' => 'Répartition des tâches',
+ 'Reportings' => 'Rapports',
+ 'Task repartition for "%s"' => 'Répartition des tâches pour « %s »',
+ 'Analytics' => 'Analytique',
+ 'Subtask' => 'Sous-tâche',
+ 'My subtasks' => 'Mes sous-tâches',
+ 'User repartition' => 'Répartition des utilisateurs',
+ '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.',
+ 'Edit Project' => 'Modifier le projet',
+ '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',
+ 'The project id must be an integer' => 'L\'id du projet doit être un entier',
+ 'The status must be an integer' => 'Le status doit être un entier',
+ 'The subtask id is required' => 'L\'id de la sous-tâche est obligatoire',
+ 'The subtask id must be an integer' => 'L\'id de la sous-tâche doit être en entier',
+ 'The task id is required' => 'L\'id de la tâche est obligatoire',
+ 'The task id must be an integer' => 'L\'id de la tâche doit être en entier',
+ 'The user id must be an integer' => 'L\'id de l\'utilisateur doit être en entier',
+ 'This value is required' => 'Cette valeur est obligatoire',
+ 'This value must be numeric' => 'Cette valeur doit être numérique',
+ 'Unable to create this task.' => 'Impossible de créer cette tâche',
+ 'Cumulative flow diagram' => 'Diagramme de flux cumulé',
+ 'Cumulative flow diagram for "%s"' => 'Diagramme de flux cumulé pour « %s »',
+ 'Daily project summary' => 'Résumé journalier du projet',
+ 'Daily project summary export' => 'Export du résumé journalier du projet',
+ 'Daily project summary export for "%s"' => 'Export du résumé quotidien du projet pour « %s »',
+ 'Exports' => 'Exports',
+ 'This export contains the number of tasks per column grouped per day.' => 'Cet export contient le nombre de tâches par colonne groupé par jour.',
+ 'Nothing to preview...' => 'Rien à prévisualiser...',
+ 'Preview' => 'Prévisualiser',
+ 'Write' => 'Écrire',
+ 'Active swimlanes' => 'Swimlanes actives',
+ 'Add a new swimlane' => 'Ajouter une nouvelle swimlane',
+ 'Change default swimlane' => 'Modifier la swimlane par défaut',
+ '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.',
+ 'Swimlane removed successfully.' => 'Swimlane supprimée avec succès.',
+ '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éparé 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',
+ 'Subtasks exportation for "%s"' => 'Exportation des sous-tâches pour le projet « %s »',
+ 'Task Title' => 'Titre de la tâche',
+ 'Untitled' => 'Sans nom',
+ 'Application default' => 'Valeur par défaut de l\'application',
+ 'Language:' => 'Langue :',
+ 'Timezone:' => 'Fuseau horaire :',
+ 'All columns' => 'Toutes les colonnes',
+ 'Calendar for "%s"' => 'Agenda pour le projet « %s »',
+ 'Filter by column' => 'Filtrer par colonne',
+ 'Filter by status' => 'Filtrer par status',
+ 'Calendar' => 'Agenda',
+ 'Next' => 'Suivant',
+ '#%d' => 'n˚%d',
+ 'Filter by color' => 'Filtrer par couleur',
+ 'Filter by swimlane' => 'Filtrer par swimlanes',
+ 'All swimlanes' => 'Toutes les swimlanes',
+ 'All colors' => 'Toutes les couleurs',
+ 'All status' => 'Tous les états',
+ 'Add a comment logging moving the task between columns' => 'Ajouter un commentaire de log lorsqu\'une tâche est déplacée dans une autre colonne',
+ 'Moved to column %s' => 'Tâche déplacée à la colonne %s',
+ 'Change description' => 'Changer la description',
+ 'User dashboard' => 'Tableau de bord de l\'utilisateur',
+ 'Allow only one subtask in progress at the same time for a user' => 'Autoriser une seule sous-tâche en progrès en même temps pour un utilisateur',
+ 'Edit column "%s"' => 'Modifier la colonne « %s »',
+ 'Enable time tracking for subtasks' => 'Activer la feuille de temps pour les sous-tâches',
+ 'Select the new status of the subtask: "%s"' => 'Selectionnez le nouveau statut de la sous-tâche : « %s »',
+ 'Subtask timesheet' => 'Feuille de temps des sous-tâches',
+ 'There is nothing to show.' => 'Il n\'y a rien à montrer.',
+ 'Time Tracking' => 'Feuille de temps',
+ '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 ?',
+ 'Change dashboard view' => 'Changer la vue du tableau de bord',
+ 'Show/hide activities' => 'Afficher/cacher les activités',
+ 'Show/hide projects' => 'Afficher/cacher les projets',
+ 'Show/hide subtasks' => 'Afficher/cacher les sous-tâches',
+ 'Show/hide tasks' => 'Afficher/cacher les tâches',
+ 'Disable login form' => 'Désactiver le formulaire d\'authentification',
+ 'Show/hide calendar' => 'Afficher/cacher le calendrier',
+ 'User calendar' => 'Calendrier de l\'utilisateur',
+ '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' => 'Age de la tâche en jours',
+ 'Days in this column' => 'Jours dans cette colonne',
+ '%dd' => '%dj',
+ 'Add a link' => 'Ajouter un lien',
+ 'Add a new link' => 'Ajouter un nouveau lien',
+ 'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien : « %s » ?',
+ 'Do you really want to remove this link with task #%d?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche n°%d ?',
+ 'Field required' => 'Champ obligatoire',
+ 'Link added successfully.' => 'Lien créé avec succès.',
+ 'Link updated successfully.' => 'Lien mis à jour avec succès.',
+ 'Link removed successfully.' => 'Lien supprimé avec succès.',
+ 'Link labels' => 'Libellé des liens',
+ 'Link modification' => 'Modification d\'un lien',
+ 'Links' => 'Liens',
+ 'Link settings' => 'Paramètres des liens',
+ 'Opposite label' => 'Nom du libellé opposé',
+ 'Remove a link' => 'Supprimer un lien',
+ 'Task\'s links' => 'Liens des tâches',
+ 'The labels must be different' => 'Les libellés doivent être différents',
+ 'There is no link.' => 'Il n\'y a aucun lien.',
+ 'This label must be unique' => 'Ce libellé doit être unique',
+ 'Unable to create your link.' => 'Impossible d\'ajouter ce lien.',
+ 'Unable to update your link.' => 'Impossible de mettre à jour ce lien.',
+ 'Unable to remove this link.' => 'Impossible de supprimer ce lien.',
+ 'relates to' => 'est liée à',
+ 'blocks' => 'bloque',
+ 'is blocked by' => 'est bloquée par',
+ 'duplicates' => 'duplique',
+ 'is duplicated by' => 'est dupliquée par',
+ 'is a child of' => 'est un enfant de',
+ 'is a parent of' => 'est un parent de',
+ 'targets milestone' => 'vise l\'étape importante',
+ 'is a milestone of' => 'est une étape importante incluant',
+ 'fixes' => 'corrige',
+ 'is fixed by' => 'est corrigée par',
+ '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',
+ 'Close dialog box' => 'Fermer une boite de dialogue',
+ 'Submit a form' => 'Enregistrer un formulaire',
+ 'Board view' => 'Page du tableau',
+ 'Keyboard shortcuts' => 'Raccourcis clavier',
+ 'Open board switcher' => 'Ouvrir le sélecteur de tableau',
+ 'Application' => 'Application',
+ 'Filter recently updated' => 'Récemment modifié',
+ 'since %B %e, %Y at %k:%M %p' => 'depuis le %d/%m/%Y à %H:%M',
+ 'More filters' => 'Plus de filtres',
+ '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 :',
+ 'Remove hourly rate' => 'Supprimer un taux horaire',
+ 'Do you really want to remove this hourly rate?' => 'Voulez-vous vraiment supprimer ce taux horaire ?',
+ 'Hourly rates' => 'Taux horaires',
+ 'Hourly rate' => 'Taux horaire',
+ 'Currency' => 'Devise',
+ 'Effective date' => 'Date d\'effet',
+ 'Add new rate' => 'Ajouter un nouveau taux horaire',
+ 'Rate removed successfully.' => 'Taux horaire supprimé avec succès.',
+ 'Unable to remove this rate.' => 'Impossible de supprimer ce taux horaire.',
+ 'Unable to save the hourly rate.' => 'Impossible de sauvegarder ce taux horaire.',
+ 'Hourly rate created successfully.' => 'Taux horaire créé avec succès.',
+ 'Start time' => 'Date de début',
+ 'End time' => 'Date de fin',
+ 'Comment' => 'Commentaire',
+ 'All day' => 'Toute la journée',
+ 'Day' => 'Jour',
+ 'Manage timetable' => 'Gérer les horaires',
+ 'Overtime timetable' => 'Heures supplémentaires',
+ 'Time off timetable' => 'Heures d\'absences',
+ 'Timetable' => 'Horaires',
+ 'Work timetable' => 'Horaires travaillés',
+ 'Week timetable' => 'Horaires de la semaine',
+ 'Day timetable' => 'Horaire d\'une journée',
+ 'From' => 'Depuis',
+ 'To' => 'À',
+ 'Time slot created successfully.' => 'Créneau horaire créé avec succès.',
+ 'Unable to save this time slot.' => 'Impossible de sauvegarder ce créneau horaire.',
+ 'Time slot removed successfully.' => 'Créneau horaire supprimé avec succès.',
+ 'Unable to remove this time slot.' => 'Impossible de supprimer ce créneau horaire.',
+ 'Do you really want to remove this time slot?' => 'Voulez-vous vraiment supprimer ce créneau horaire ?',
+ 'Remove time slot' => 'Supprimer un créneau horaire',
+ 'Add new time slot' => 'Ajouter un créneau horaire',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Ces horaires sont utilisés lorsque la case « Toute la journée » est cochée pour les heures d\'absences ou supplémentaires programmées.',
+ 'Files' => 'Fichiers',
+ 'Images' => 'Images',
+ 'Private project' => 'Projet privé',
+ 'Amount' => 'Montant',
+ 'AUD - Australian Dollar' => 'AUD - Dollar australien',
+ 'Budget' => 'Budget',
+ 'Budget line' => 'Ligne budgétaire',
+ 'Budget line removed successfully.' => 'Ligne budgétaire supprimée avec succès.',
+ 'Budget lines' => 'Lignes budgétaire',
+ 'CAD - Canadian Dollar' => 'CAD - Dollar canadien',
+ 'CHF - Swiss Francs' => 'CHF - Franc suisse',
+ 'Cost' => 'Coût',
+ 'Cost breakdown' => 'Détail des coûts',
+ 'Custom Stylesheet' => 'Feuille de style personalisée',
+ 'download' => 'télécharger',
+ 'Do you really want to remove this budget line?' => 'Voulez-vous vraiment supprimer cette ligne budgétaire ?',
+ 'EUR - Euro' => 'EUR - Euro',
+ 'Expenses' => 'Dépenses',
+ 'GBP - British Pound' => 'GBP - Livre sterling',
+ 'INR - Indian Rupee' => 'INR - Roupie indienne',
+ 'JPY - Japanese Yen' => 'JPY - Yen',
+ 'New budget line' => 'Nouvelle ligne budgétaire',
+ 'NZD - New Zealand Dollar' => 'NZD - Dollar néo-zélandais',
+ 'Remove a budget line' => 'Supprimer une ligne budgétaire',
+ 'Remove budget line' => 'Supprimer une ligne budgétaire',
+ 'RSD - Serbian dinar' => 'RSD - Dinar serbe',
+ 'The budget line have been created successfully.' => 'La ligne de budgétaire a été créée avec succès.',
+ 'Unable to create the budget line.' => 'Impossible de créer cette ligne budgétaire.',
+ 'Unable to remove this budget line.' => 'Impossible de supprimer cette ligne budgétaire.',
+ 'USD - US Dollar' => 'USD - Dollar américain',
+ 'Remaining' => 'Restant',
+ 'Destination column' => 'Colonne de destination',
+ 'Move the task to another column when assigned to a user' => 'Déplacer la tâche dans une autre colonne lorsque celle-ci est assignée à quelqu\'un',
+ 'Move the task to another column when assignee is cleared' => 'Déplacer la tâche dans une autre colonne lorsque celle-ci n\'est plus assignée',
+ 'Source column' => 'Colonne d\'origine',
+ 'Show subtask estimates (forecast of future work)' => 'Afficher l\'estimation des sous-tâches (prévision du travail à venir)',
+ 'Transitions' => 'Transitions',
+ 'Executer' => 'Exécutant',
+ 'Time spent in the column' => 'Temps passé dans la colonne',
+ 'Task transitions' => 'Transitions des tâches',
+ 'Task transitions export' => 'Export des transitions des tâches',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Ce rapport contient tous les mouvements de colonne pour chaque tâche avec la date, l\'utilisateur et le temps passé pour chaque transition.',
+ 'Currency rates' => 'Taux de change des devises',
+ 'Rate' => 'Taux',
+ 'Change reference currency' => 'Changer la monnaie de référence',
+ 'Add a new currency rate' => 'Ajouter un nouveau taux pour une devise',
+ 'Currency rates are used to calculate project budget.' => 'Le cours des devises est utilisé pour calculer le budget des projets.',
+ 'Reference currency' => 'Devise de référence',
+ 'The currency rate have been added successfully.' => 'Le taux de change a été ajouté avec succès.',
+ 'Unable to add this currency rate.' => 'Impossible d\'ajouter ce taux de change',
+ 'Send notifications to a Slack channel' => 'Envoyer les notifications sur un salon de discussion Slack',
+ 'Webhook URL' => 'URL du webhook',
+ 'Help on Slack integration' => 'Aide sur l\'intégration avec Slack',
+ '%s remove the assignee of the task %s' => '%s a enlevé la personne assignée à la tâche %s',
+ 'Send notifications to Hipchat' => 'Envoyer les notifications vers Hipchat',
+ 'API URL' => 'URL de l\'api',
+ 'Room API ID or name' => 'Nom ou identifiant du salon de discussion',
+ 'Room notification token' => 'Jeton de sécurité du salon de discussion',
+ 'Help on Hipchat integration' => 'Aide sur l\'intégration avec Hipchat',
+ 'Enable Gravatar images' => 'Activer les images Gravatar',
+ 'Information' => 'Informations',
+ 'Check two factor authentication code' => 'Vérification du code pour l\'authentification à deux-facteurs',
+ 'The two factor authentication code is not valid.' => 'Le code pour l\'authentification à deux-facteurs n\'est pas valide.',
+ '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é secrete 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).',
+ 'Screenshot taken %s' => 'Capture d\'écran prise le %s',
+ 'Add a screenshot' => 'Ajouter une capture d\'écran',
+ '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',
+ 'Postmark (incoming emails)' => 'Postmark (emails entrants)',
+ 'Help on Postmark integration' => 'Aide sur l\'intégration avec Postmark',
+ 'Mailgun (incoming emails)' => 'Mailgun (emails entrants)',
+ 'Help on Mailgun integration' => 'Aide sur l\'intégration avec Mailgun',
+ 'Sendgrid (incoming emails)' => 'Sendgrid (emails entrants)',
+ 'Help on Sendgrid integration' => 'Aide sur l\'intégration avec Sendgrid',
+ '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...',
+ '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',
+ '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',
+ 'Factor to calculate new due date' => 'Facteur pour calculer la nouvelle date d\'échéance',
+ 'Timeframe to calculate new due date' => 'Échelle de temps pour calculer la nouvelle date d\'échéance',
+ 'Base date to calculate new due date' => 'Date à utiliser pour calculer la nouvelle date d\'échéance',
+ 'Action date' => 'Date de l\'action',
+ 'Base date to calculate new due date: ' => 'Date utilisée pour calculer la nouvelle date d\'échéance : ',
+ 'This task has created this child task: ' => 'Cette tâche a créée la tâche enfant : ',
+ 'Day(s)' => 'Jour(s)',
+ 'Existing due date' => 'Date d\'échéance existante',
+ 'Factor to calculate new due date: ' => 'Facteur pour calculer la nouvelle date d\'échéance : ',
+ 'Month(s)' => 'Mois',
+ 'Recurrence' => 'Récurrence',
+ 'This task has been created by: ' => 'Cette tâche a été créée par :',
+ 'Recurrent task has been generated:' => 'Une tâche récurrente a été générée :',
+ 'Timeframe to calculate new due date: ' => 'Échelle de temps pour calculer la nouvelle date d\'échéance : ',
+ 'Trigger to generate recurrent task: ' => 'Déclencheur pour générer la tâche récurrente : ',
+ 'When task is closed' => 'Lorsque la tâche est fermée',
+ 'When task is moved from first column' => 'Lorsque la tâche est déplacée en dehors de la première colonne',
+ 'When task is moved to last column' => 'Lorsque la tâche est déplacée dans la dernière colonne',
+ 'Year(s)' => 'Année(s)',
+ 'Jabber (XMPP)' => 'Jabber (XMPP)',
+ 'Send notifications to Jabber' => 'Envoyer les notifications vers Jabber',
+ 'XMPP server address' => 'Adresse du serveur XMPP',
+ 'Jabber domain' => 'Nom de domaine Jabber',
+ 'Jabber nickname' => 'Pseudonyme Jabber',
+ 'Multi-user chat room' => 'Salon de discussion multi-utilisateurs',
+ 'Help on Jabber integration' => 'Aide sur l\'intégration avec Jabber',
+ 'The server address must use this format: "tcp://hostname:5222"' => 'L\'adresse du serveur doit utiliser le format suivant : « tcp://hostname:5222 »',
+ 'Calendar settings' => 'Paramètres du calendrier',
+ 'Project calendar view' => 'Vue en mode projet du calendrier',
+ 'Project settings' => 'Paramètres des projets',
+ 'Show subtasks based on the time tracking' => 'Afficher les sous-tâches basé sur le suivi du temps',
+ 'Show tasks based on the creation date' => 'Afficher les tâches en fonction de la date de création',
+ 'Show tasks based on the start date' => 'Afficher les tâches en fonction de la date de début',
+ 'Subtasks time tracking' => 'Suivi du temps par rapport aux sous-tâches',
+ 'User calendar view' => 'Vue en mode utilisateur du calendrier',
+ 'Automatically update the start date' => 'Mettre à jour automatiquement la date de début',
+ 'iCal feed' => 'Abonnement iCal',
+ 'Preferences' => 'Préférences',
+ 'Security' => 'Sécurité',
+ 'Two factor authentication disabled' => 'Authentification à deux facteurs désactivé',
+ 'Two factor authentication enabled' => 'Authentification à deux facteurs activée',
+ 'Unable to update this user.' => 'Impossible de mettre à jour cet utilisateur.',
+ 'There is no user management for private projects.' => 'Il n\'y a pas de gestion d\'utilisateurs pour les projets privés.',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
new file mode 100644
index 00000000..3d8302c5
--- /dev/null
+++ b/app/Locale/hu_HU/translations.php
@@ -0,0 +1,925 @@
+<?php
+
+return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => ' ',
+ 'None' => 'Nincs',
+ 'edit' => 'szerkesztés',
+ 'Edit' => 'Szerkesztés',
+ 'remove' => 'törlés',
+ 'Remove' => 'Törlés',
+ 'Update' => 'Frissítés',
+ 'Yes' => 'Igen',
+ 'No' => 'Nem',
+ 'cancel' => 'Mégsem',
+ 'or' => 'vagy',
+ 'Yellow' => 'Sárga',
+ 'Blue' => 'Kék',
+ 'Green' => 'Zöld',
+ 'Purple' => 'Lila',
+ 'Red' => 'Piros',
+ 'Orange' => 'Narancs',
+ 'Grey' => 'Szürke',
+ 'Save' => 'Mentés',
+ 'Login' => 'Bejelentkezés',
+ 'Official website:' => 'Hivatalos honlap:',
+ 'Unassigned' => 'Nincs felelős',
+ 'View this task' => 'Feladat megtekintése',
+ 'Remove user' => 'Felhasználó törlése',
+ 'Do you really want to remove this user: "%s"?' => 'Tényleg törli ezt a felhasználót: "%s"?',
+ 'New user' => 'Új felhasználó',
+ 'All users' => 'Minden felhasználó',
+ 'Username' => 'Felhasználónév',
+ 'Password' => 'Jelszó',
+ 'Default project' => 'Alapértelmezett projekt',
+ 'Administrator' => 'Rendszergazda',
+ 'Sign in' => 'Jelentkezzen be',
+ 'Users' => 'Felhasználók',
+ 'No user' => 'Nincs felhasználó',
+ 'Forbidden' => 'tiltott',
+ 'Access Forbidden' => 'Hozzáférés megtagadva',
+ 'Only administrators can access to this page.' => 'Csak a rendszergazdák férhetnek hozzá az oldalhoz.',
+ 'Edit user' => 'Felhasználó módosítása',
+ 'Logout' => 'Kilépés',
+ 'Bad username or password' => 'Rossz felhasználónév vagy jelszó',
+ 'users' => 'felhasználók',
+ 'projects' => 'projektek',
+ 'Edit project' => 'Projekt szerkesztése',
+ 'Name' => 'Név',
+ 'Activated' => 'Aktiválva',
+ 'Projects' => 'Projektek',
+ 'No project' => 'Nincs projekt',
+ 'Project' => 'Projekt',
+ 'Status' => 'Állapot',
+ 'Tasks' => 'Feladat',
+ 'Board' => 'Tábla',
+ 'Actions' => 'Műveletek',
+ 'Inactive' => 'Inaktív',
+ 'Active' => 'Aktív',
+ 'Column %d' => 'Oszlop %d',
+ 'Add this column' => 'Oszlop hozzáadása',
+ '%d tasks on the board' => '%d feladat a táblán',
+ '%d tasks in total' => 'Összesen %d feladat',
+ 'Unable to update this board.' => 'Nem lehet frissíteni a táblát.',
+ 'Edit board' => 'Tábla szerkesztése',
+ 'Disable' => 'Letiltás',
+ 'Enable' => 'Engedélyezés',
+ 'New project' => 'Új projekt',
+ 'Do you really want to remove this project: "%s"?' => 'Valóban törölni akarja ezt a projektet: "%s"?',
+ 'Remove project' => 'Projekt törlése',
+ 'Boards' => 'Táblák',
+ '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',
+ 'Add Column' => 'Oszlopot hozzáad',
+ 'Project "%s"' => 'Projekt "%s"',
+ 'Nobody assigned' => 'Nincs felelős',
+ 'Assigned to %s' => 'Felelős: %s',
+ 'Remove a column' => 'Oszlop törlése',
+ 'Remove a column from a board' => 'Oszlop törlése a tábláról',
+ 'Unable to remove this column.' => 'Az oszlop törlése nem lehetséges.',
+ 'Do you really want to remove this column: "%s"?' => 'Valóban törölni akarja ezt az oszlopot: "%s"?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Az oszlophoz rendelt ÖSSZES FELADAT TÖRLŐDNI FOG!',
+ 'Settings' => 'Beállítások',
+ 'Application settings' => 'Alkalmazás beállítások',
+ 'Language' => 'Nyelv',
+ 'Webhook token:' => 'Webhook token:',
+ 'API token:' => 'API token:',
+ 'More information' => 'További információ',
+ 'Database size:' => 'Adatbázis méret:',
+ 'Download the database' => 'Adatbázis letöltése',
+ 'Optimize the database' => 'Adatbázis optimalizálása',
+ '(VACUUM command)' => '(VACUUM parancs)',
+ '(Gzip compressed Sqlite file)' => '(Gzip tömörített SQLite fájl)',
+ 'User settings' => 'Felhasználói beállítások',
+ 'My default project:' => 'Alapértelmezett projekt: ',
+ 'Close a task' => 'Feladat lezárása',
+ 'Do you really want to close this task: "%s"?' => 'Tényleg le akarja zárni ezt a feladatot: "%s"?',
+ 'Edit a task' => 'Feladat módosítása',
+ 'Column' => 'Oszlop',
+ 'Color' => 'Szín',
+ 'Assignee' => 'Felelős',
+ 'Create another task' => 'Új feladat létrehozása',
+ 'New task' => 'Új feladat',
+ '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.',
+ 'Add a new task' => 'Új feladat hozzáadása',
+ 'The username is required' => 'Felhasználói név szükséges',
+ 'The maximum length is %d characters' => 'A maximális hossz %d karakter',
+ 'The minimum length is %d characters' => 'A minimális hossza %d karakter',
+ 'The password is required' => 'Jelszó szükséges',
+ 'This value must be an integer' => 'Ez az érték csak egész szám lehet',
+ 'The username must be unique' => 'A felhasználó nevének egyedinek kell lennie',
+ 'The username must be alphanumeric' => 'A felhasználói név csak alfanumerikus lehet (betűk és számok)',
+ 'The user id is required' => 'A felhasználói azonosítót meg kell adni',
+ 'Passwords don\'t match' => 'A jelszavak nem egyeznek',
+ 'The confirmation is required' => 'Megerősítés szükséges',
+ 'The column is required' => 'Az oszlopot meg kell adni',
+ 'The project is required' => 'A projektet meg kell adni',
+ 'The color is required' => 'A színt meg kell adni',
+ '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',
+ 'The language is required' => 'A nyelvet meg kell adni',
+ 'There is no active project, the first step is to create a new project.' => 'Nincs aktív projekt. Először létre kell hozni egy projektet.',
+ 'Settings saved successfully.' => 'A beállítások sikeresen mentve.',
+ 'Unable to save your settings.' => 'A beállítások mentése sikertelen.',
+ 'Database optimization done.' => 'Adatbázis optimalizálás kész.',
+ 'Your project have been created successfully.' => 'Projekt sikeresen létrehozva',
+ 'Unable to create your project.' => 'Projekt létrehozása sikertelen.',
+ 'Project updated successfully.' => 'Projekt sikeresen frissítve.',
+ 'Unable to update this project.' => 'Projekt frissítése sikertelen.',
+ 'Unable to remove this project.' => 'Projekt törlése sikertelen.',
+ 'Project removed successfully.' => 'Projekt sikeresen törölve.',
+ 'Project activated successfully.' => 'Projekt sikeresen aktiválva.',
+ 'Unable to activate this project.' => 'Projekt aktiválása sikertelen.',
+ 'Project disabled successfully.' => 'Projekt sikeresen letiltva.',
+ 'Unable to disable this project.' => 'Projekt letiltása sikertelen.',
+ 'Unable to open this task.' => 'A feladat felnyitása sikertelen.',
+ 'Task opened successfully.' => 'Feladat sikeresen megnyitva .',
+ 'Unable to close this task.' => 'A feladat lezárása sikertelen.',
+ 'Task closed successfully.' => 'Feladat sikeresen lezárva.',
+ 'Unable to update your task.' => 'Feladat frissítése sikertelen.',
+ 'Task updated successfully.' => 'Feladat sikeresen frissítve.',
+ 'Unable to create your task.' => 'Feladat létrehozása sikertelen.',
+ 'Task created successfully.' => 'Feladat sikeresen létrehozva.',
+ 'User created successfully.' => 'Felhasználó létrehozva.',
+ 'Unable to create your user.' => 'Felhasználó létrehozása sikertelen.',
+ 'User updated successfully.' => 'Felhasználó sikeresen frissítve.',
+ 'Unable to update your user.' => 'Felhasználó frissítése sikertelen.',
+ 'User removed successfully.' => 'Felhasználó sikeresen törölve.',
+ 'Unable to remove this user.' => 'Felhasználó törlése sikertelen.',
+ 'Board updated successfully.' => 'Tábla sikeresen frissítve.',
+ 'Ready' => 'Előkészítés',
+ 'Backlog' => 'Napló',
+ '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',
+ 'No task' => 'Nincs feladat',
+ 'Completed tasks' => 'Elvégzett feladatok',
+ 'List of projects' => 'Projektek listája',
+ 'Completed tasks for "%s"' => 'Elvégzett feladatok: %s',
+ '%d closed tasks' => '%d lezárt feladat',
+ 'No task for this project' => 'Nincs feladat ebben a projektben',
+ 'Public link' => 'Nyilvános link',
+ 'There is no column in your project!' => 'Nincs oszlop a projektben!',
+ 'Change assignee' => 'Felelős módosítása',
+ 'Change assignee for the task "%s"' => 'Feladat felelősének módosítása: "%s"',
+ 'Timezone' => 'Időzóna',
+ 'Sorry, I didn\'t find this information in my database!' => 'Ez az információ nem található az adatbázisban!',
+ 'Page not found' => 'Az oldal nem található',
+ 'Complexity' => 'Bonyolultság',
+ 'limit' => 'határ',
+ 'Task limit' => 'Maximális számú feladat',
+ 'Task count' => 'Feladatok száma',
+ 'This value must be greater than %d' => 'Az értéknek nagyobbnak kell lennie, mint %d',
+ 'Edit project access list' => 'Projekt hozzáférés módosítása',
+ 'Edit users access' => 'Felhasználók hozzáférésének módosítása',
+ 'Allow this user' => 'Engedélyezi ezt a felhasználót',
+ 'Only those users have access to this project:' => 'Csak ezek a felhasználók férhetnek hozzá a projekthez:',
+ '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.',
+ 'You are not allowed to access to this project.' => 'Nincs hozzáférési joga a projekthez.',
+ 'Comments' => 'Hozzászólások',
+ 'Post comment' => 'Hozzászólás elküldése',
+ 'Write your text in Markdown' => 'Írja be a szöveget Markdown szintaxissal',
+ 'Leave a comment' => 'Írjon hozzászólást ...',
+ 'Comment is required' => 'A hozzászólás mező kötelező',
+ 'Leave a description' => 'Írjon leírást ...',
+ 'Comment added successfully.' => 'Hozzászólás sikeresen elküldve.',
+ 'Unable to create your comment.' => 'Hozzászólás létrehozása nem lehetséges.',
+ 'The description is required' => 'A leírás szükséges',
+ '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.',
+ 'Remove an action' => 'Intézkedés törlése',
+ 'Unable to remove this action.' => 'Intézkedés törlése nem lehetséges.',
+ 'Action removed successfully.' => 'Intézkedés sikeresen törölve.',
+ 'Automatic actions for the project "%s"' => 'Automatikus intézkedések a projektben: "%s"',
+ 'Defined actions' => 'Intézkedések',
+ 'Add an action' => 'Intézkedés létrehozása',
+ 'Event name' => 'Esemény neve',
+ 'Action name' => 'Intézkedés neve',
+ 'Action parameters' => 'Intézkedés paraméterei',
+ 'Action' => 'Intézkedés',
+ 'Event' => 'Esemény',
+ 'When the selected event occurs execute the corresponding action.' => 'Ha a kiválasztott esemény bekövetkezik, hajtsa végre a megfelelő intézkedéseket.',
+ 'Next step' => 'Következő lépés',
+ 'Define action parameters' => 'Határozza meg az intézkedés paramétereit',
+ 'Save this action' => 'Intézkedés mentése',
+ 'Do you really want to remove this action: "%s"?' => 'Valóban törölni akarja ezt az intézkedést: "%s"?',
+ 'Remove an automatic action' => 'Automatikus intézkedés törlése',
+ 'Close the task' => 'Feladat lezárása',
+ 'Assign the task to a specific user' => 'Feladat kiosztása megadott felhasználónak',
+ 'Assign the task to the person who does the action' => 'Feladat kiosztása az intézkedő személynek',
+ 'Duplicate the task to another project' => 'Feladat másolása másik projektbe',
+ 'Move a task to another column' => 'Feladat mozgatása másik oszlopba',
+ 'Move a task to another position in the same column' => 'Feladat mozgatása oszlopon belül',
+ 'Task modification' => 'Feladat módosítása',
+ 'Task creation' => 'Feladat létrehozása',
+ 'Open a closed task' => 'Lezárt feladat felnyitása',
+ 'Closing a task' => 'Feladat lezárása',
+ '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',
+ 'Update this comment' => 'Hozzászólás frissítése',
+ 'Comment updated successfully.' => 'Megjegyzés sikeresen frissítve.',
+ 'Unable to update your comment.' => 'Megjegyzés frissítése sikertelen.',
+ 'Remove a comment' => 'Megjegyzés törlése',
+ '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.',
+ 'Details' => 'Részletek',
+ '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ó',
+ 'Reset all tokens' => 'Reseteld az összes tokent',
+ 'All tokens have been regenerated.' => 'Minden token újra lett generálva.',
+ 'Unknown' => 'Ismeretlen',
+ 'Last logins' => 'Legutóbbi bejelentkezések',
+ 'Login date' => 'Bejelentkezés dátuma',
+ 'Authentication method' => 'Azonosítási módszer',
+ 'IP address' => 'IP-cím',
+ 'User agent' => 'User Agent',
+ 'Persistent connections' => 'Tartós (perzisztens) kapcsolatok',
+ 'No session.' => 'Nincs session.',
+ 'Expiration date' => 'Lejárati dátum',
+ 'Remember Me' => 'Emlékezz rám',
+ 'Creation date' => 'Létrehozás dátuma',
+ 'Filter by user' => 'Szűrés felhasználó szerint',
+ 'Filter by due date' => 'Szűrés határidő szerint',
+ 'Everybody' => 'Minden felhasználó',
+ 'Open' => 'Nyitott',
+ 'Closed' => 'Lezárt',
+ 'Search' => 'Keresés',
+ 'Nothing found.' => 'Nincs találat.',
+ 'Search in the project "%s"' => 'Keresés a projektben: "%s"',
+ 'Due date' => 'Határidő',
+ 'Others formats accepted: %s and %s' => 'Egyéb érvényes formátumok: "%s" és "%s"',
+ 'Description' => 'Leírás',
+ '%d comments' => '%d megjegyzés',
+ '%d comment' => '%d megjegyzés',
+ 'Email address invalid' => 'Érvénytelen e-mail cím',
+ 'Your Google Account is not linked anymore to your profile.' => 'Google Fiók már nincs a profilhoz kapcsolva.',
+ 'Unable to unlink your Google Account.' => 'Leválasztás a Google fiókról nem lehetséges.',
+ 'Google authentication failed' => 'Google azonosítás sikertelen',
+ 'Unable to link your Google Account.' => 'A Google profilhoz kapcsolás sikertelen.',
+ 'Your Google Account is linked to your profile successfully.' => 'Google fiókkal sikeresen összekapcsolva.',
+ '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 #%d' => 'Feladat #%d.',
+ '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',
+ 'Do you really want to remove this task: "%s"?' => 'Valóban törölni akarja ezt a feladatot: "%s"?',
+ 'Assign automatically a color based on a category' => 'Szín hozzárendelése automatikusan kategória alapján',
+ 'Assign automatically a category based on a color' => 'Kategória hozzárendelése automatikusan szín alapján',
+ 'Task creation or modification' => 'Feladat létrehozása vagy módosítása',
+ 'Category' => 'Kategória',
+ 'Category:' => 'Kategória:',
+ 'Categories' => 'Kategóriák',
+ 'Category not found.' => 'Kategória nem található.',
+ 'Your category have been created successfully.' => 'Kategória sikeresen létrehozva.',
+ 'Unable to create your category.' => 'A kategória létrehozása nem lehetséges.',
+ 'Your category have been updated successfully.' => 'Kategória sikeresen frissítve.',
+ 'Unable to update your category.' => 'Kategória frissítése nem lehetséges.',
+ 'Remove a category' => 'Kategória törlése',
+ 'Category removed successfully.' => 'Kategória törlése megtörtént.',
+ 'Unable to remove this category.' => 'A kategória törlése nem lehetséges.',
+ 'Category modification for the project "%s"' => 'Kategória módosítása a projektben "%s"',
+ 'Category Name' => 'Kategória neve',
+ 'Categories for the project "%s"' => 'Projekt kategóriák "%s"',
+ 'Add a new category' => 'Új kategória',
+ 'Do you really want to remove this category: "%s"?' => 'Valóban törölni akarja ezt a kategóriát: "%s"?',
+ 'Filter by category' => 'Szűrés kategória szerint',
+ 'All categories' => 'Minden kategória',
+ 'No category' => 'Nincs kategória',
+ 'The name is required' => 'A név megadása kötelező',
+ 'Remove a file' => 'Fájl törlése',
+ 'Unable to remove this file.' => 'Fájl törlése nem lehetséges.',
+ 'File removed successfully.' => 'Fájl sikeresen törölve.',
+ 'Attach a document' => 'Fájl csatolása',
+ 'Do you really want to remove this file: "%s"?' => 'Valóban törölni akarja a fájlt: "%s"?',
+ 'open' => 'nyitott',
+ 'Attachments' => 'Mellékletek',
+ 'Edit the task' => 'Feladat módosítása',
+ 'Edit the description' => 'Leírás szerkesztése',
+ 'Add a comment' => 'Új megjegyzés',
+ 'Edit a comment' => 'Megjegyzés szerkesztése',
+ 'Summary' => 'Összegzés',
+ 'Time tracking' => 'Idő követés',
+ 'Estimate:' => 'Becsült:',
+ 'Spent:' => 'Eltöltött:',
+ 'Do you really want to remove this sub-task?' => 'Valóban törölni akarja ezt a részfeladatot?',
+ 'Remaining:' => 'Hátralévő:',
+ 'hours' => 'óra',
+ 'spent' => 'eltöltött',
+ 'estimated' => 'becsült',
+ 'Sub-Tasks' => 'Részfeladatok',
+ 'Add a sub-task' => 'Részfeladat létrehozása',
+ 'Original estimate' => 'Eredeti időbecslés',
+ 'Create another sub-task' => 'További részfeladat létrehozása',
+ 'Time spent' => 'Eltöltött idő',
+ 'Edit a sub-task' => 'Részfeladat szerkesztése',
+ 'Remove a sub-task' => 'Részfeladat törlése',
+ 'The time must be a numeric value' => 'Idő csak számérték lehet',
+ 'Todo' => 'Teendő',
+ 'In progress' => 'Folyamatban',
+ 'Sub-task removed successfully.' => 'Részfeladat sikeresen törölve.',
+ 'Unable to remove this sub-task.' => 'Részfeladat törlése nem lehetséges.',
+ 'Sub-task updated successfully.' => 'Részfeladat sikeresen frissítve.',
+ 'Unable to update your sub-task.' => 'Részfeladat frissítése nem lehetséges.',
+ 'Unable to create your sub-task.' => 'Részfeladat létrehozása nem lehetséges.',
+ 'Sub-task added successfully.' => 'Részfeladat sikeresen létrehozva.',
+ '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',
+ 'Your GitHub account was successfully linked to your profile.' => 'GitHub fiók sikeresen csatolva a profilhoz.',
+ 'Unable to link your GitHub Account.' => 'Nem lehet csatolni a GitHub fiókot.',
+ 'GitHub authentication failed' => 'GitHub azonosítás sikertelen',
+ 'Your GitHub account is no longer linked to your profile.' => 'GitHub fiók már nincs profilhoz kapcsolva.',
+ 'Unable to unlink your GitHub Account.' => 'GitHub fiók leválasztása nem lehetséges.',
+ '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',
+ 'End Date' => 'Befejezés dátuma',
+ 'Execute' => 'Végrehajt',
+ 'Task Id' => 'Feladat ID',
+ 'Creator' => 'Készítette',
+ 'Modification date' => 'Módosítás dátuma',
+ 'Completion date' => 'Befejezés határideje',
+ 'Clone' => 'Másolat',
+ 'Clone Project' => 'Projekt másolása',
+ 'Project cloned successfully.' => 'A projekt sikeresen másolva.',
+ 'Unable to clone this project.' => 'A projekt másolása sikertelen.',
+ 'Email notifications' => 'E-mail értesítések',
+ 'Enable email notifications' => 'E-mail értesítések engedélyezése',
+ 'Task position:' => 'Feladat helye:',
+ 'The task #%d have been opened.' => 'Feladat #%d megnyitva.',
+ 'The task #%d have been closed.' => 'Feladat #%d lezárva.',
+ 'Sub-task updated' => 'Részfeladat frissítve',
+ 'Title:' => 'Cím',
+ 'Status:' => 'Állapot:',
+ 'Assignee:' => 'Felelős:',
+ 'Time tracking:' => 'Idő követés:',
+ 'New sub-task' => 'Új részfeladat',
+ 'New attachment added "%s"' => 'Új melléklet "%s" hozzáadva.',
+ 'Comment updated' => 'Megjegyzés frissítve',
+ 'New comment posted by %s' => 'Új megjegyzés %s',
+ 'List of due tasks for the project "%s"' => 'Projekt esedékes feladatai: "%s"',
+ 'New attachment' => 'Új melléklet',
+ 'New comment' => 'Új megjegyzés',
+ 'New subtask' => 'Új részfeladat',
+ 'Subtask updated' => 'Részfeladat frissítve',
+ 'Task updated' => 'Feladat frissítve',
+ 'Task closed' => 'Feladat lezárva',
+ 'Task opened' => 'Feladat megnyitva',
+ '[%s][Due tasks]' => '[%s] [Esedékes feladatok]',
+ '[Kanboard] Notification' => '[Kanboard] értesítés',
+ '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',
+ 'Category management' => 'Kategóriák kezelése',
+ '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',
+ 'Active projects' => 'Aktív projektek',
+ 'Inactive projects' => 'Inaktív projektek',
+ 'Public access disabled' => 'Nyilvános hozzáférés letiltva',
+ 'Do you really want to disable this project: "%s"?' => 'Tényleg szeretné letiltani ezt a projektet: "%s"',
+ 'Do you really want to duplicate this project: "%s"?' => 'Tényleg szeretné megkettőzni ezt a projektet: "%s"',
+ 'Do you really want to enable this project: "%s"?' => 'Tényleg szeretné engedélyezni ezt a projektet: "%s"',
+ 'Project activation' => 'Projekt aktiválás',
+ 'Move the task to another project' => 'Feladat áthelyezése másik projektbe',
+ 'Move to another project' => 'Áthelyezés másik projektbe',
+ 'Do you really want to duplicate this task?' => 'Tényleg szeretné megkettőzni ezt a feladatot?',
+ 'Duplicate a task' => 'Feladat másolása',
+ 'External accounts' => 'Külső fiókok',
+ 'Account type' => 'Fiók típusa',
+ 'Local' => 'Helyi',
+ 'Remote' => 'Távoli',
+ 'Enabled' => 'Engedélyezve',
+ 'Disabled' => 'Letiltva',
+ 'Google account linked' => 'Google fiók összekapcsolva',
+ 'Github account linked' => 'GitHub fiók összekapcsolva',
+ 'Username:' => 'Felhasználónév:',
+ 'Name:' => 'Név:',
+ 'Email:' => 'E-mail:',
+ 'Default project:' => 'Alapértelmezett projekt:',
+ '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.',
+ 'Change category for the task "%s"' => 'Feladat kategória módosítása "%s"',
+ 'Change category' => 'Kategória módosítása',
+ '%s updated the task %s' => '%s frissítette a feladatot %s',
+ '%s opened the task %s' => '%s megnyitott a feladatot %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s átmozgatta a feladatot %s #%d pozícióba a "%s" oszlopban',
+ '%s moved the task %s to the column "%s"' => '%s átmozgatta a feladatot %s "%s" oszlopba',
+ '%s created the task %s' => '%s létrehozta a feladatot %s',
+ '%s closed the task %s' => '%s lezárta a feladatot %s',
+ '%s created a subtask for the task %s' => '%s létrehozott egy részfeladat a feladathoz %s',
+ '%s updated a subtask for the task %s' => '%s frissített egy részfeladatot a feladathoz %s',
+ 'Assigned to %s with an estimate of %s/%sh' => '%s-nek kiosztva %s/%s óra becsült idő mellett',
+ 'Not assigned, estimate of %sh' => 'Nincs kiosztva, becsült idő: %s óra',
+ '%s updated a comment on the task %s' => '%s frissítette a megjegyzését a feladatban %s',
+ '%s commented the task %s' => '%s megjegyzést fűzött a feladathoz %s',
+ '%s\'s activity' => 'Tevékenységek: %s',
+ 'No activity.' => 'Nincs tevékenység.',
+ 'RSS feed' => 'RSS feed',
+ '%s updated a comment on the task #%d' => '%s frissített egy megjegyzést a feladatban #%d',
+ '%s commented on the task #%d' => '%s megjegyzést tett a feladathoz #%d',
+ '%s updated a subtask for the task #%d' => '%s frissített egy részfeladatot a feladatban #%d',
+ '%s created a subtask for the task #%d' => '%s létrehozott egy részfeladatot a feladatban #%d',
+ '%s updated the task #%d' => '%s frissítette a feladatot #%d',
+ '%s created the task #%d' => '%s létrehozta a feladatot #%d',
+ '%s closed the task #%d' => '%s lezárta a feladatot #%d',
+ '%s open the task #%d' => '%s megnyitotta a feladatot #%d',
+ '%s moved the task #%d to the column "%s"' => '%s átmozgatta a feladatot #%d a "%s" oszlopba',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s átmozgatta a feladatot #%d a %d pozícióba a "%s" oszlopban',
+ 'Activity' => 'Tevékenységek',
+ 'Default values are "%s"' => 'Az alapértelmezett értékek: %s',
+ 'Default columns for new projects (Comma-separated)' => 'Alapértelmezett oszlopok az új projektekben (vesszővel elválasztva)',
+ 'Task assignee change' => 'Felelős módosítása',
+ '%s change the assignee of the task #%d to %s' => '%s a felelőst módosította #%d %s',
+ '%s changed the assignee of the task %s to %s' => '%s a felelőst %s módosította: %s',
+ 'Column Change' => 'Oszlop változtatás',
+ 'Position Change' => 'Pozíció változtatás',
+ 'Assignee Change' => 'Felelős változtatá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ó',
+ 'Database driver:' => 'Adatbázis motor:',
+ 'Board settings' => 'Tábla beállítások',
+ 'URL and token' => 'URL és tokenek',
+ 'Webhook settings' => 'Webhook beállítások',
+ 'URL for task creation:' => 'Feladat létrehozás URL:',
+ 'Reset token' => 'Token újragenerálása',
+ 'API endpoint:' => 'API végpont:',
+ 'Refresh interval for private board' => 'Privát táblák frissítési intervalluma',
+ 'Refresh interval for public board' => 'Nyilvános táblák frissítési intervalluma',
+ 'Task highlight period' => 'Feladat kiemelés időtartama',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Mennyi ideig tekintendő egy feladat "mostanában" módosítottnak (másodpercben) (0: funkció letiltva, alapértelmezés szerint 2 nap)',
+ '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"',
+ 'New private project' => 'Új privát projekt',
+ 'This project is private' => 'Ez egy privát projekt',
+ 'Type here to create a new sub-task' => 'Ide írva létrehozhat egy új részfeladatot',
+ 'Add' => 'Hozzáadás',
+ '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.',
+ 'My tasks' => 'Feladataim',
+ 'Activity stream' => 'Legutóbbi tevékenységek',
+ 'Dashboard' => 'Vezérlőpult',
+ 'Confirmation' => 'Megerősítés',
+ 'Allow everybody to access to this project' => 'A projekt elérése mindenkinek engedélyezett',
+ 'Everybody have access to this project.' => 'Mindenki elérheti a projektet',
+ 'Webhooks' => 'Webhook',
+ 'API' => 'API',
+ 'Integration' => 'Integráció',
+ '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',
+ 'Configure' => 'Beállítások',
+ 'Project management' => 'Projekt menedzsment',
+ 'My projects' => 'Projektjeim',
+ 'Columns' => 'Oszlopok',
+ 'Task' => 'Feladat',
+ 'Your are not member of any project.' => 'Ön nem tagja projektnek.',
+ 'Percentage' => 'Százalék',
+ 'Number of tasks' => 'A feladatok száma',
+ 'Task distribution' => 'Feladatelosztás',
+ 'Reportings' => 'Jelentések',
+ 'Task repartition for "%s"' => 'Feladat újraosztása: %s',
+ 'Analytics' => 'Analitika',
+ 'Subtask' => 'Részfeladat',
+ 'My subtasks' => 'Részfeladataim',
+ 'User repartition' => 'Felhasználó újrafelosztás',
+ 'User repartition for "%s"' => 'Felhasználó újrafelosztás: %s',
+ 'Clone this project' => 'Projekt másolása',
+ 'Column removed successfully.' => 'Oszlop sikeresen törölve.',
+ 'Edit Project' => 'Projekt szerkesztése',
+ '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',
+ 'The project id must be an integer' => 'A projekt ID csak egész szám lehet',
+ 'The status must be an integer' => 'Az állapot csak egész szám lehet',
+ 'The subtask id is required' => 'A részfeladat ID-t meg kell adni',
+ 'The subtask id must be an integer' => 'A részfeladat ID csak egész szám lehet',
+ 'The task id is required' => 'A feladat ID-t meg kell adni',
+ 'The task id must be an integer' => 'A feladat ID csak egész szám lehet',
+ 'The user id must be an integer' => 'A felhasználói ID csak egész szám lehet',
+ 'This value is required' => 'Ez a mező kötelező',
+ 'This value must be numeric' => 'Ez a mező csak szám lehet',
+ 'Unable to create this task.' => 'A feladat nem hozható létre,',
+ 'Cumulative flow diagram' => 'Kumulatív Flow Diagram',
+ 'Cumulative flow diagram for "%s"' => 'Kumulatív Flow Diagram: %s',
+ 'Daily project summary' => 'Napi projektösszefoglaló',
+ 'Daily project summary export' => 'Napi projektösszefoglaló exportálása',
+ 'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása: %s',
+ 'Exports' => 'Exportálások',
+ 'This export contains the number of tasks per column grouped per day.' => 'Ez az export tartalmazza a feladatok számát oszloponként összesítve, napokra lebontva.',
+ 'Nothing to preview...' => 'Nincs semmi az előnézetben ...',
+ 'Preview' => 'Előnézet',
+ 'Write' => 'Szerkesztés',
+ 'Active swimlanes' => 'Aktív folyamatok',
+ 'Add a new swimlane' => 'Új folyamat',
+ 'Change default swimlane' => 'Alapértelmezett folyamat változtatás',
+ '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ó',
+ 'Swimlane removed successfully.' => 'Folyamat sikeresen törölve.',
+ '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',
+ 'Subtasks exportation for "%s"' => 'Részfeladatok exportálása: %s',
+ 'Task Title' => 'Feladat címe',
+ 'Untitled' => 'Névtelen',
+ 'Application default' => 'Alkalmazás alapértelmezett',
+ 'Language:' => 'Nyelv:',
+ 'Timezone:' => 'Időzóna:',
+ 'All columns' => 'Minden oszlop',
+ 'Calendar for "%s"' => 'Naptár: %s',
+ 'Filter by column' => 'Szűrés oszlop szerint',
+ 'Filter by status' => 'Szűrés állapot szerint',
+ 'Calendar' => 'Naptár',
+ 'Next' => 'Következő',
+ '#%d' => '#%d',
+ 'Filter by color' => 'Szűrés szín szerint',
+ 'Filter by swimlane' => 'Szűrés folyamat szerint',
+ 'All swimlanes' => 'Minden folyamat',
+ 'All colors' => 'Minden szín',
+ 'All status' => 'Minden állapot',
+ 'Add a comment logging moving the task between columns' => 'Feladat oszlopok közötti mozgatását megjegyzésben feltüntetni',
+ 'Moved to column %s' => '%s oszlopba áthelyezve',
+ 'Change description' => 'Leírás szerkesztés',
+ 'User dashboard' => 'Felhasználói vezérlőpult',
+ 'Allow only one subtask in progress at the same time for a user' => 'Egyszerre csak egy folyamatban levő részfeladat engedélyezése a felhasználóknak',
+ 'Edit column "%s"' => 'Oszlop szerkesztés: %s',
+ 'Enable time tracking for subtasks' => 'Idő követés engedélyezése a részfeladatokhoz',
+ 'Select the new status of the subtask: "%s"' => 'Részfeladat állapot változtatás: %s',
+ 'Subtask timesheet' => 'Részfeladat idővonal',
+ 'There is nothing to show.' => 'Nincs megjelenítendő adat.',
+ 'Time Tracking' => 'Idő követés',
+ '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?',
+ 'Change dashboard view' => 'Vezérlőpult megjelenés változtatás',
+ 'Show/hide activities' => 'Tevékenységek megjelenítése/elrejtése',
+ 'Show/hide projects' => 'Projektek megjelenítése/elrejtése',
+ 'Show/hide subtasks' => 'Részfeladatok megjelenítése/elrejtése',
+ 'Show/hide tasks' => 'Feladatok megjelenítése/elrejtése',
+ 'Disable login form' => 'Bejelentkező képernyő tiltása',
+ 'Show/hide calendar' => 'Naptár megjelenítés/elrejtés',
+ 'User calendar' => 'Naptár',
+ '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',
+ 'Days in this column' => 'Napok ebben az oszlopban',
+ '%dd' => '%dd',
+ 'Add a link' => 'Hivatkozás hozzáadása',
+ 'Add a new link' => 'Új hivatkozás hozzáadása',
+ 'Do you really want to remove this link: "%s"?' => 'Biztos törölni akarja a hivatkozást: "%s"?',
+ 'Do you really want to remove this link with task #%d?' => 'Biztos törölni akarja a(z) #%s. feladatra mutató hivatkozást?',
+ 'Field required' => 'Kötelező mező',
+ 'Link added successfully.' => 'Hivatkozás sikeresen létrehozva.',
+ 'Link updated successfully.' => 'Hivatkozás sikeresen frissítve.',
+ 'Link removed successfully.' => 'Hivatkozás sikeresen törölve.',
+ 'Link labels' => 'Hivatkozás címkék',
+ 'Link modification' => 'Hivatkozás módosítás',
+ 'Links' => 'Hivatkozások',
+ 'Link settings' => 'Hivatkozás beállítasok',
+ 'Opposite label' => 'Ellenekező címke',
+ 'Remove a link' => 'Hivatkozás törlése',
+ 'Task\'s links' => 'Feladat hivatkozások',
+ 'The labels must be different' => 'A címkék nem lehetnek azonosak',
+ 'There is no link.' => 'Nincs hivatkozás.',
+ 'This label must be unique' => 'A címkének egyedinek kell lennie.',
+ 'Unable to create your link.' => 'Hivatkozás létrehozása sikertelen.',
+ 'Unable to update your link.' => 'Hivatkozás frissítése sikertelen.',
+ 'Unable to remove this link.' => 'Hivatkozás törlése sikertelen.',
+ 'relates to' => 'hozzá tartozik:',
+ 'blocks' => 'letiltva:',
+ 'is blocked by' => 'letitoltta:',
+ 'duplicates' => 'eredeti:',
+ 'is duplicated by' => 'másolat:',
+ 'is a child of' => 'szülője:',
+ 'is a parent of' => 'gyermeke:',
+ 'targets milestone' => 'megcélzott mérföldkő:',
+ 'is a milestone of' => 'ehhez a mérföldkőhöz tartozik:',
+ 'fixes' => 'javítás:',
+ 'is fixed by' => 'javította:',
+ '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',
+ 'Close dialog box' => 'Ablak bezárása',
+ 'Submit a form' => 'Űrlap beküldése',
+ 'Board view' => 'Tábla nézet',
+ 'Keyboard shortcuts' => 'Billentyű kombinációk',
+ 'Open board switcher' => 'Tábla választó lenyitása',
+ 'Application' => 'Alkalmazás',
+ 'Filter recently updated' => 'Szűrés az utolsó módosítás ideje szerint',
+ 'since %B %e, %Y at %k:%M %p' => '%Y. %m. %d. %H:%M óta',
+ 'More filters' => 'További szűrők',
+ '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:',
+ 'Remove hourly rate' => 'Órabér törlése',
+ 'Do you really want to remove this hourly rate?' => 'Valóban törölni kívánja az órabért?',
+ 'Hourly rates' => 'Órabérek',
+ 'Hourly rate' => 'Órabér',
+ 'Currency' => 'Pénznem',
+ 'Effective date' => 'Hatálybalépés ideje',
+ 'Add new rate' => 'Új bér',
+ 'Rate removed successfully.' => 'Bér sikeresen törölve.',
+ 'Unable to remove this rate.' => 'Bér törlése sikertelen.',
+ 'Unable to save the hourly rate.' => 'Órabér mentése sikertelen.',
+ 'Hourly rate created successfully.' => 'Órabér sikeresen mentve.',
+ 'Start time' => 'Kezdés ideje',
+ 'End time' => 'Végzés ideje',
+ 'Comment' => 'Megjegyzés',
+ 'All day' => 'Egész nap',
+ 'Day' => 'Nap',
+ 'Manage timetable' => 'Időbeosztás kezelése',
+ 'Overtime timetable' => 'Túlóra időbeosztás',
+ 'Time off timetable' => 'Szabadság időbeosztás',
+ 'Timetable' => 'Időbeosztás',
+ 'Work timetable' => 'Munka időbeosztás',
+ 'Week timetable' => 'Heti időbeosztás',
+ 'Day timetable' => 'Napi időbeosztás',
+ 'From' => 'Feladó:',
+ 'To' => 'Címzett:',
+ 'Time slot created successfully.' => 'Időszelet sikeresen létrehozva.',
+ 'Unable to save this time slot.' => 'Időszelet mentése sikertelen.',
+ 'Time slot removed successfully.' => 'Időszelet sikeresen törölve.',
+ 'Unable to remove this time slot.' => 'Időszelet törlése sikertelen.',
+ 'Do you really want to remove this time slot?' => 'Biztos törli ezt az időszeletet?',
+ 'Remove time slot' => 'Időszelet törlése',
+ 'Add new time slot' => 'Új Időszelet',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Ez az időbeosztás van használatban ha az "egész nap" jelölőnégyzet be van jelölve a tervezett szabadságnál és túlóránál.',
+ 'Files' => 'Fájlok',
+ 'Images' => 'Képek',
+ 'Private project' => 'Privát projekt',
+ 'Amount' => 'Összeg',
+ 'AUD - Australian Dollar' => 'AUD - Ausztrál dollár',
+ 'Budget' => 'Költségvetés',
+ 'Budget line' => 'Költségvetési tétel',
+ 'Budget line removed successfully.' => 'Költségvetési tétel sikeresen törölve.',
+ 'Budget lines' => 'Költségvetési tételek',
+ 'CAD - Canadian Dollar' => 'CAD - Kanadai dollár',
+ 'CHF - Swiss Francs' => 'CHF - Svájci frank',
+ 'Cost' => 'Költség',
+ 'Cost breakdown' => 'Költség visszaszámlálás',
+ 'Custom Stylesheet' => 'Egyéni sítluslap',
+ 'download' => 'letöltés',
+ 'Do you really want to remove this budget line?' => 'Biztos törölni akarja ezt a költségvetési tételt?',
+ 'EUR - Euro' => 'EUR - Euro',
+ 'Expenses' => 'Kiadások',
+ 'GBP - British Pound' => 'GBP - Angol font',
+ 'INR - Indian Rupee' => 'INR - Indiai rúpia',
+ 'JPY - Japanese Yen' => 'JPY - Japán Yen',
+ 'New budget line' => 'Új költségvetési tétel',
+ 'NZD - New Zealand Dollar' => 'NZD - Új-Zélandi dollár',
+ 'Remove a budget line' => 'Költségvetési tétel törlése',
+ 'Remove budget line' => 'Költségvetési tétel törlése',
+ 'RSD - Serbian dinar' => 'RSD - Szerb dínár',
+ 'The budget line have been created successfully.' => 'Költségvetési tétel sikeresen létrehozva.',
+ 'Unable to create the budget line.' => 'Költségvetési tétel létrehozása sikertelen.',
+ 'Unable to remove this budget line.' => 'Költségvetési tétel törlése sikertelen.',
+ 'USD - US Dollar' => 'USD - Amerikai ollár',
+ 'Remaining' => 'Maradék',
+ 'Destination column' => 'Cél oszlop',
+ 'Move the task to another column when assigned to a user' => 'Feladat másik oszlopba helyezése felhasználóhoz rendélés után',
+ 'Move the task to another column when assignee is cleared' => 'Feladat másik oszlopba helyezése felhasználóhoz rendélés törlésekor',
+ 'Source column' => 'Forrás oszlop',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ // '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' => '',
+ // 'Currency rates are used to calculate project budget.' => '',
+ // 'Reference currency' => '',
+ // 'The currency rate have been added successfully.' => '',
+ // 'Unable to add this currency rate.' => '',
+ // 'Send notifications to a Slack channel' => '',
+ // 'Webhook URL' => '',
+ // 'Help on Slack integration' => '',
+ // '%s remove the assignee of the task %s' => '',
+ // 'Send notifications to Hipchat' => '',
+ // 'API URL' => '',
+ // 'Room API ID or name' => '',
+ // 'Room notification token' => '',
+ // 'Help on Hipchat integration' => '',
+ // '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.' => '',
+ // 'SEK - Swedish Krona' => '',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
+);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
new file mode 100644
index 00000000..09ae351d
--- /dev/null
+++ b/app/Locale/it_IT/translations.php
@@ -0,0 +1,925 @@
+<?php
+
+return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => '.',
+ 'None' => 'Nessuno',
+ 'edit' => 'modificare',
+ 'Edit' => 'Modificare',
+ 'remove' => 'cancellare',
+ 'Remove' => 'Cancellare',
+ 'Update' => 'Aggiornare',
+ 'Yes' => 'Si',
+ 'No' => 'No',
+ 'cancel' => 'annullare',
+ 'or' => 'o',
+ 'Yellow' => 'Giallo',
+ 'Blue' => 'Blu',
+ 'Green' => 'Verde',
+ 'Purple' => 'Viola',
+ 'Red' => 'Rosso',
+ 'Orange' => 'Arancione',
+ 'Grey' => 'Grigio',
+ 'Save' => 'Salvare',
+ 'Login' => 'Entra',
+ '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',
+ 'All users' => 'Tutti gli utenti',
+ 'Username' => 'Nome utente',
+ 'Password' => 'Password',
+ 'Default project' => 'Progetto predefinito',
+ 'Administrator' => 'Amministratore',
+ 'Sign in' => 'Iscriversi',
+ 'Users' => 'Utenti',
+ 'No user' => 'Nessun utente',
+ 'Forbidden' => 'Vietato',
+ 'Access Forbidden' => 'Accesso vietato',
+ 'Only administrators can access to this page.' => 'Solo gli amministratori possono accedere a questa pagina.',
+ 'Edit user' => 'Modificare un utente',
+ 'Logout' => 'Uscire',
+ 'Bad username or password' => 'Utente o password errati',
+ 'users' => 'utenti',
+ 'projects' => 'progetti',
+ 'Edit project' => 'Modificare progetto',
+ 'Name' => 'Nome',
+ 'Activated' => 'Attivo',
+ 'Projects' => 'Progetti',
+ 'No project' => 'Nessun progetto',
+ 'Project' => 'Progetto',
+ 'Status' => 'Stato',
+ 'Tasks' => 'Compiti',
+ 'Board' => 'Bacheca',
+ 'Actions' => 'Azioni',
+ 'Inactive' => 'Inattivo',
+ 'Active' => 'Attivo',
+ 'Column %d' => 'Colonna %d',
+ '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',
+ 'New project' => 'Nuovo progetto',
+ 'Do you really want to remove this project: "%s"?' => 'Veramente vuoi eliminare questo progetto: « %s » ?',
+ 'Remove project' => 'Cancellare il progetto',
+ 'Boards' => 'Bacheche',
+ 'Edit the board for "%s"' => 'Modificare la bacheca per « %s »',
+ 'All projects' => 'Tutti i progetti',
+ 'Change columns' => 'Cambiare le colonne',
+ 'Add a new column' => 'Aggiungere una nuova colonna',
+ 'Title' => 'Titolo',
+ 'Add Column' => 'Aggiungere colonna',
+ 'Project "%s"' => 'progetto « %s »',
+ '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!',
+ 'Settings' => 'Impostazioni',
+ 'Application settings' => 'Impostazioni dell\'applicazione',
+ 'Language' => 'Lingua',
+ 'Webhook token:' => 'Identificatore (token) per i webhooks :',
+ 'API token:' => 'Token dell\'API:',
+ 'More information' => 'Più informazioni',
+ 'Database size:' => 'Dimensioni della base dati:',
+ 'Download the database' => 'Scaricare la base dati',
+ 'Optimize the database' => 'Ottimizare la base dati',
+ '(VACUUM command)' => '(Comando VACUUM)',
+ '(Gzip compressed Sqlite file)' => '(File Sqlite compresso in Gzip)',
+ 'User settings' => 'Impostazioni di utente',
+ 'My default project:' => 'Il mio progetto predefinito:',
+ 'Close a task' => 'Chiudere un compito',
+ 'Do you really want to close this task: "%s"?' => 'Veramente desideri chiudere questo compito: « %s » ?',
+ 'Edit a task' => 'Modificare un compito',
+ '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',
+ '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',
+ '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',
+ 'The password is required' => 'Si richiede una password',
+ 'This value must be an integer' => 'questo valore deve essere un intero',
+ 'The username must be unique' => 'Il nome di utente deve essere unico',
+ 'The username must be alphanumeric' => 'Il nome di utente deve essere alfanumerico',
+ 'The user id is required' => 'Si richiede l\'identificatore dell\'utente',
+ 'Passwords don\'t match' => 'Le password non corrispondono',
+ 'The confirmation is required' => 'Si richiede una conferma',
+ 'The column is required' => 'Si richiede una colonna',
+ 'The project is required' => 'Si richiede il progetto',
+ 'The color is required' => 'Si richiede il colore',
+ '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',
+ 'The language is required' => 'Si richiede una lingua',
+ 'There is no active project, the first step is to create a new project.' => 'Non ci sono progetti attivi, il primo passo consiste in creare un nuovo progetto.',
+ 'Settings saved successfully.' => 'Impostazioni salvate correttamente.',
+ 'Unable to save your settings.' => 'Non si possono 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.',
+ '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.',
+ 'Project removed successfully.' => 'Progetto cancellato correttamente.',
+ 'Project activated successfully.' => 'Progetto attivato correttamente.',
+ 'Unable to activate this project.' => 'Non si può 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.',
+ 'User created successfully.' => 'Utente creato correttamente.',
+ 'Unable to create your user.' => 'Non si può creare l\'utente.',
+ 'User updated successfully.' => 'Utente aggiornato correttamente.',
+ 'Unable to update your user.' => 'Non si può aggiornare questo utente.',
+ 'User removed successfully.' => 'Utente cancellato correttamente.',
+ 'Unable to remove this user.' => 'Non si può 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',
+ 'No task' => 'Nessun compito',
+ 'Completed tasks' => 'Compiti fatti',
+ 'List of projects' => 'Lista di progetti',
+ 'Completed tasks for "%s"' => 'Compiti fatti da « %s »',
+ '%d closed tasks' => '%d compiti chiusi',
+ 'No task for this project' => 'Nessun compito per questo progetto',
+ 'Public link' => 'Link pubblico',
+ 'There is no column in your project!' => 'Non c\'è nessuna colonna per questo progetto!',
+ 'Change assignee' => 'Cambiare la persona assegnata',
+ 'Change assignee for the task "%s"' => 'Cambiare la persona assegnata per il compito « %s »',
+ 'Timezone' => 'Fuso orario',
+ 'Sorry, I didn\'t find this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!',
+ 'Page not found' => 'Pagina non trovata',
+ 'Complexity' => 'Complessità',
+ 'limit' => 'limite',
+ 'Task limit' => 'Numero massimo di compiti',
+ 'Task count' => 'Numero di compiti',
+ 'This value must be greater than %d' => 'questo valore deve essere maggiore di %d',
+ 'Edit project access list' => 'Modificare i permessi del progetto',
+ 'Edit users access' => 'Modificare i permessi degli utenti',
+ 'Allow this user' => 'Permettere a questo utente',
+ 'Only those users have access to this project:' => 'Solo questi utenti hanno accesso a questo progetto:',
+ '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',
+ 'User' => 'Utente',
+ 'Nobody have access to this project.' => 'Nessuno ha accesso a questo progetto.',
+ 'You are not allowed to access to this project.' => 'Non hai l\'accesso a questo progetto.',
+ 'Comments' => 'Commenti',
+ 'Post comment' => 'Mandare commento',
+ 'Write your text in Markdown' => 'Scrivi il testo in Markdown',
+ 'Leave a comment' => 'Lasciare un commento',
+ 'Comment is required' => 'Si richiede un commento',
+ 'Leave a description' => 'Lasciare una descrizione',
+ 'Comment added successfully.' => 'Commenti aggiunti correttamente.',
+ 'Unable to create your comment.' => 'Non si può creare questo commento.',
+ 'The description is required' => 'Si richiede una descrizione',
+ 'Edit this task' => 'Modificare questo compito',
+ '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' => '',
+ '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.',
+ 'Remove an action' => 'Cancellare un\'azione',
+ 'Unable to remove this action.' => 'Non si può cancellare questa azione.',
+ 'Action removed successfully.' => 'Azione cancellata correttamente.',
+ 'Automatic actions for the project "%s"' => 'Azioni automatiche per questo progetto « %s »',
+ 'Defined actions' => 'Azioni definite',
+ 'Add an action' => 'Aggiungi un\'azione',
+ 'Event name' => 'Nome dell\'evento',
+ 'Action name' => 'Nome dell\'azione',
+ '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',
+ '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',
+ 'Close the task' => 'Chiudere questo compito',
+ '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',
+ 'Move a task to another position in the same column' => 'Muovere un compito in un\'altra posizione sulla stessa colonna',
+ 'Task modification' => 'Modifica di un compito',
+ 'Task creation' => 'Creazione di un compito',
+ 'Open a closed task' => 'Riaprire un compito',
+ 'Closing a task' => 'Chiudere un compito',
+ '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',
+ 'Update this comment' => 'Aggiornare questo commento',
+ 'Comment updated successfully.' => 'Commento aggiornato correttamente.',
+ 'Unable to update your comment.' => 'Non si può aggiornare questo commento.',
+ 'Remove a comment' => 'Cancellare 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.',
+ 'Details' => 'Dettagli',
+ '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',
+ 'Reset all tokens' => 'Azzerare gli identificatori (tokens) di sicurezza ',
+ 'All tokens have been regenerated.' => 'Tutti gli identificatori (tokens) sono stati rigenerati.',
+ 'Unknown' => 'Sconociuto',
+ 'Last logins' => 'Ultimi ingressi',
+ 'Login date' => 'Data di ingresso',
+ 'Authentication method' => 'Metodo di autenticazzione',
+ 'IP address' => 'Indirizzo IP',
+ 'User agent' => 'Navigatore',
+ 'Persistent connections' => 'Connessioni persistenti',
+ 'No session.' => 'Non esiste sessione.',
+ 'Expiration date' => 'Data di scadenza',
+ 'Remember Me' => 'Ricordami',
+ 'Creation date' => 'Data di creazione',
+ 'Filter by user' => 'Filtrato mediante utente',
+ 'Filter by due date' => 'Filtrare attraverso data di scadenza',
+ 'Everybody' => 'Tutti',
+ 'Open' => 'Aperto',
+ 'Closed' => 'Chiuso',
+ 'Search' => 'Cercare',
+ 'Nothing found.' => 'Non si è trovato nulla.',
+ 'Search in the project "%s"' => 'Cercare nel progetto "%s"',
+ 'Due date' => 'Data di scadenza',
+ 'Others formats accepted: %s and %s' => 'Altri formati accettati: %s y %s',
+ 'Description' => 'Descrizione',
+ '%d comments' => '%d commenti',
+ '%d comment' => '%d commento',
+ 'Email address invalid' => 'Indirizzo e-mail sbagliato',
+ 'Your Google Account is not linked anymore to your profile.' => 'Il suo account Google non è più collegato al suo profilo',
+ 'Unable to unlink your Google Account.' => 'Non si può svincolare l\'account di Google.',
+ 'Google authentication failed' => 'Autenticazione con Google non riuscita',
+ 'Unable to link your Google Account.' => 'Non si può collegare il tuo account di Google.',
+ 'Your Google Account is linked to your profile successfully.' => 'Il tuo account di Google è stato collegato correttamente al tuo profilo.',
+ '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 #%d' => 'Compito numero %d',
+ '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',
+ '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.',
+ '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',
+ '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',
+ 'Categories for the project "%s"' => 'Categorie per il progetto',
+ 'Add a new category' => 'Aggiungere una nuova categoria',
+ 'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare questa categoria: "%s"?',
+ 'Filter by category' => 'Filtrare attraverso categoria',
+ '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.',
+ 'File removed successfully.' => 'File cancellato correttamente.',
+ 'Attach a document' => 'Allegare un documento',
+ 'Do you really want to remove this file: "%s"?' => 'Vuoi veramente cancellare questo file: "%s"?',
+ 'open' => 'aprire',
+ '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',
+ 'Summary' => 'Sommario',
+ 'Time tracking' => 'Time tracking',
+ 'Estimate:' => 'Stimato:',
+ 'Spent:' => 'Trascorso:',
+ 'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sotto-compito?',
+ 'Remaining:' => 'Rimangono',
+ 'hours' => 'ore',
+ 'spent' => 'trascorse',
+ 'estimated' => 'stimate',
+ 'Sub-Tasks' => 'Sotto-compiti',
+ 'Add a sub-task' => 'Aggiungere un sotto-compito',
+ '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',
+ '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',
+ 'Your GitHub account was successfully linked to your profile.' => 'Il suo account di Github è stato collegato correttamente col tuo profilo.',
+ 'Unable to link your GitHub Account.' => 'Non si può collegarre il tuo account di Github.',
+ 'GitHub authentication failed' => 'Autenticazione con GitHub non riuscita',
+ 'Your GitHub account is no longer linked to your profile.' => 'Il tuo account di Github non è più collegato al tuo profilo.',
+ 'Unable to unlink your GitHub Account.' => 'Non si può collegare il tuo account di Github.',
+ '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',
+ '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 »',
+ 'Start Date' => 'Data d\'inizio',
+ 'End Date' => 'Data di fine',
+ 'Execute' => 'Eseguire',
+ 'Task Id' => 'Identificatore del compito',
+ 'Creator' => 'Creatore',
+ 'Modification date' => 'Data di modifica',
+ 'Completion date' => 'Data di termine',
+ 'Clone' => 'Clona',
+ 'Clone Project' => 'Clona il progetto',
+ 'Project cloned successfully.' => 'Progetto clonato con successo.',
+ 'Unable to clone this project.' => 'Impossibile clonare questo progetto',
+ 'Email notifications' => 'Notifiche email',
+ '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',
+ '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 »',
+ 'Comment updated' => 'Commento aggiornato',
+ 'New comment posted by %s' => 'Nuovo commento aggiunto da « %s »',
+ 'List of due tasks for the project "%s"' => 'Lista dei compiti scaduti per il progetto « %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',
+ '[%s][Due tasks]' => '[%s][Compiti scaduti]',
+ '[Kanboard] Notification' => '[Kanboard] Notifica',
+ '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',
+ 'Public access' => 'Accesso pubblico',
+ 'Category management' => 'Gestione delle categorie',
+ 'User management' => 'Gestione utenti',
+ 'Active tasks' => 'Compiti attivi',
+ 'Disable public access' => 'Disabilita l\'accesso pubblico',
+ 'Enable public access' => 'Abilita l\'accesso pubblico',
+ 'Active projects' => 'Progetti attivi',
+ 'Inactive projects' => 'Progetti inattivi',
+ '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 duplicate this project: "%s"?' => 'Vuoi davvero duplicare questo progetto: "%s"?',
+ 'Do you really want to enable this project: "%s"?' => 'Vuoi davvero abilitare questo 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',
+ 'External accounts' => 'Account esterni',
+ 'Account type' => 'Tipo di account',
+ 'Local' => 'Locale',
+ 'Remote' => 'Remoto',
+ 'Enabled' => 'Abilitato',
+ 'Disabled' => 'Disabilitato',
+ 'Google account linked' => 'Account Google collegato',
+ 'Github account linked' => 'Account Github collegato',
+ // 'Username:' => '',
+ 'Name:' => 'Nome:',
+ // 'Email:' => '',
+ 'Default project:' => 'Progetto di default',
+ 'Notifications:' => 'Notifiche:',
+ 'Notifications' => 'Notifiche',
+ 'Group:' => 'Gruppo',
+ 'Regular user' => 'Utente regolare',
+ 'Account type:' => 'Tipo di account',
+ 'Edit profile' => 'Modifica il 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' => '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',
+ '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\'s activity' => 'Attività di %s',
+ 'No activity.' => 'Nessuna attività.',
+ '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"',
+ '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' => '% 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 %',
+ 'Column Change' => 'Cambio di colonna',
+ 'Position Change' => 'Posizione cambiata',
+ 'Assignee Change' => 'Assegnatario cambiato',
+ 'New password for the user "%s"' => 'Nuova password per l\'utente "%s"',
+ 'Choose an event' => 'Scegli un evento',
+ 'Github commit received' => 'Commit di Github ricevuto',
+ 'Github issue opened' => 'Issue di Github ricevuto',
+ 'Github issue closed' => 'Issue di Github chiusa',
+ 'Github issue reopened' => 'Issue di Github riaperta',
+ 'Github issue assignee change' => 'Assegnatario dell\'issue di Github cambiato',
+ 'Github issue label change' => 'Etichetta dell\'issue di Github cambiata',
+ 'Create a task from an external provider' => 'Crea un compito da un provider esterno',
+ 'Change the assignee based on an external username' => 'Cambia l\'assegnatario basandosi su un username esterno',
+ 'Change the category based on an external label' => 'Cambia la categoria basandosi su un\'etichetta esterna',
+ 'Reference' => 'Referenza',
+ 'Reference: %s' => 'Referenza :%s',
+ 'Label' => 'Etichetta',
+ // 'Database' => '',
+ 'About' => 'Info',
+ '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:',
+ '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)',
+ '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',
+ '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',
+ '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',
+ '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',
+ // 'Webhooks' => '',
+ // 'API' => '',
+ 'Integration' => 'Integrazione',
+ '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',
+ 'Configure' => 'Configura',
+ 'Project management' => 'Gestione del progetto',
+ 'My projects' => 'I miei progetti',
+ 'Columns' => 'Colonne',
+ 'Task' => 'Compito',
+ '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',
+ 'Reportings' => 'Rapporti',
+ 'Task repartition for "%s"' => 'Ripartizione compiti per "%s"',
+ // 'Analytics' => '',
+ 'Subtask' => 'Sotto-compiti',
+ 'My subtasks' => 'I miei sotto-compiti',
+ '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',
+ 'Edit Project' => 'Modifica progetto',
+ '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 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',
+ '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"',
+ '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',
+ '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"?',
+ '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',
+ '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',
+ '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.',
+ '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"',
+ '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',
+ 'Untitled' => 'Senza titolo',
+ 'Application default' => 'Default dell\'applicazione',
+ 'Language:' => 'Lingua',
+ 'Timezone:' => 'Fuso Orario',
+ 'All columns' => 'Tutte le colonne',
+ 'Calendar for "%s"' => 'Calendario per "%s"',
+ 'Filter by column' => 'Filtra per colonna',
+ 'Filter by status' => 'Filtra per status',
+ 'Calendar' => 'Calendario',
+ 'Next' => 'Prossimo',
+ // '#%d' => '',
+ 'Filter by color' => 'Filtra per colore',
+ 'Filter by swimlane' => 'Filtra per corsia',
+ 'All swimlanes' => 'Tutte le corsie',
+ 'All colors' => 'Tutti i colori',
+ 'All status' => 'Tutti gli stati',
+ 'Add a comment logging moving the task between columns' => 'Aggiungi un commento per tracciare lo spostamento del compito tra colonne',
+ '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',
+ 'Edit column "%s"' => 'Modifica la colonna "%s"',
+ 'Enable time tracking for subtasks' => 'Abilita la gestione del tempo per i sotto-compiti',
+ 'Select the new status of the subtask: "%s"' => 'Selziona il nuovo status per il sotto-compito: "%s"',
+ 'Subtask timesheet' => 'Timesheet del sotto-compito',
+ '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',
+ 'Which parts of the project do you want to duplicate?' => 'Quali parti del progetto vuoi duplicare?',
+ 'Change dashboard view' => 'Cambia la vista della bacheca',
+ 'Show/hide activities' => 'Mostra/nascondi attività',
+ 'Show/hide projects' => 'Mostra/nascondi progetti',
+ 'Show/hide subtasks' => 'Mostra/nascondi sotto-compiti',
+ 'Show/hide tasks' => 'Mostra/nascondi compiti',
+ 'Disable login form' => 'Disabilita form di login',
+ 'Show/hide calendar' => 'Mostra/nascondi calendario',
+ 'User calendar' => 'Calendario utente',
+ 'Bitbucket commit received' => 'Commit ricevuto da Bitbucket',
+ 'Bitbucket webhooks' => 'Webhooks di Bitbucket',
+ 'Help on Bitbucket webhooks' => 'Guida ai Webhooks di Bitbucket',
+ 'Start' => 'Inizio',
+ 'End' => 'Fine',
+ 'Task age in days' => 'Anzianità del compito in giorni',
+ '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?',
+ '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',
+ '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.',
+ 'relates to' => 'si riferisce a',
+ 'blocks' => 'blocca',
+ 'is blocked by' => 'è bloccato da',
+ 'duplicates' => 'duplica',
+ 'is duplicated by' => 'è duplicato da',
+ 'is a child of' => 'è un figlio di',
+ 'is a parent of' => 'è un genitore di',
+ 'targets milestone' => 'punta alla milestone',
+ 'is a milestone of' => 'è una milestone di',
+ 'fixes' => 'sistema',
+ 'is fixed by' => 'è sistemato da',
+ 'This task' => 'Questo compito',
+ // '<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',
+ 'Submit a form' => 'Invia i dati',
+ 'Board view' => 'Vista bacheca',
+ 'Keyboard shortcuts' => 'Scorciatoie da tastiera',
+ 'Open board switcher' => 'Apri il selezionatore di bacheche',
+ 'Application' => 'Applicazione',
+ 'Filter recently updated' => 'Filtri recentemente aggiornati',
+ 'since %B %e, %Y at %k:%M %p' => 'dal %B %e, %Y alle %k:%M %p',
+ 'More filters' => 'Più filtri',
+ 'Compact view' => 'Vista compatta',
+ 'Horizontal scrolling' => 'Scrolling orizzontale',
+ 'Compact/wide view' => 'Vista compatta/estesa',
+ 'No results match:' => 'Nessun risultato trovato:',
+ 'Remove hourly rate' => 'Rimuovi tariffa oraria',
+ 'Do you really want to remove this hourly rate?' => 'Vuoi davvero rimuovere questa tariffa oraria?',
+ 'Hourly rates' => 'Tariffe orarie',
+ 'Hourly rate' => 'Tariffa oraria',
+ 'Currency' => 'Valuta',
+ 'Effective date' => 'Data effettiva',
+ 'Add new rate' => 'Aggiungi una nuova tariffa',
+ 'Rate removed successfully.' => 'Tariffa rimossa con successo.',
+ 'Unable to remove this rate.' => 'Impossibile rimuovere questa tariffa.',
+ 'Unable to save the hourly rate.' => 'Impossibile salvare la tariffa oraria.',
+ 'Hourly rate created successfully.' => 'Tariffa oraria creata con successo.',
+ 'Start time' => 'Data di inizio',
+ 'End time' => 'Data di completamento',
+ 'Comment' => 'Commento',
+ 'All day' => 'Tutto il giorno',
+ 'Day' => 'Giorno',
+ 'Manage timetable' => 'Gestisci orario',
+ 'Overtime timetable' => 'Straordinari',
+ 'Time off timetable' => 'Fuori orario',
+ 'Timetable' => 'Orario',
+ 'Work timetable' => 'Orario di lavoro',
+ 'Week timetable' => 'Orario settimanale',
+ 'Day timetable' => 'Orario giornaliero',
+ 'From' => 'Da',
+ 'To' => 'A',
+ 'Time slot created successfully.' => 'Fascia oraria creata con successo.',
+ 'Unable to save this time slot.' => 'Impossibile creare questa fascia oraria.',
+ 'Time slot removed successfully.' => 'Fascia oraria rimossa con successo.',
+ 'Unable to remove this time slot.' => 'Impossibile rimuovere questa fascia oraria.',
+ 'Do you really want to remove this time slot?' => 'Vuoi davvero rimuovere questa fascia oraria?',
+ 'Remove time slot' => 'Rimuovi fascia oraria',
+ 'Add new time slot' => 'Aggiungi nuova fascia oraria',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Questo orario è utilizzato quando la casella "tutto il giorno" è selezionata per i fuori orari e per gli straordinari',
+ // 'Files' => '',
+ 'Images' => 'Immagini',
+ 'Private project' => 'Progetto privato',
+ 'Amount' => 'Totale',
+ 'AUD - Australian Dollar' => 'AUD - Dollari Australiani',
+ 'Budget' => 'Bilancio',
+ 'Budget line' => 'Limite di bilancio',
+ 'Budget line removed successfully.' => 'Limite al bilancio rimosso con successo.',
+ 'Budget lines' => 'Limiti al bilancio',
+ 'CAD - Canadian Dollar' => 'CAD - Dollari Canadesi',
+ 'CHF - Swiss Francs' => 'CHF - Franchi Svizzeri',
+ 'Cost' => 'Costi',
+ 'Cost breakdown' => 'Abbattimento dei costi',
+ 'Custom Stylesheet' => 'CSS personalizzato',
+ // 'download' => '',
+ 'Do you really want to remove this budget line?' => 'Vuoi davvero rimuovere questo limite al bilancio?',
+ // 'EUR - Euro' => '',
+ 'Expenses' => 'Spese',
+ 'GBP - British Pound' => 'GBP - Pound Inglesi',
+ 'INR - Indian Rupee' => 'INR - Rupie Indiani',
+ 'JPY - Japanese Yen' => 'JPY - Yen Giapponesi',
+ 'New budget line' => 'Nuovo limite al bilancio',
+ 'NZD - New Zealand Dollar' => 'NZD - Dollari della Nuova Zelanda',
+ 'Remove a budget line' => 'Rimuovi un limite al bilancio',
+ 'Remove budget line' => 'Rimuovi limite di bilancio',
+ 'RSD - Serbian dinar' => 'RSD - Dinar Serbi',
+ 'The budget line have been created successfully.' => 'Il limite al bilancio è stato creato correttamente',
+ 'Unable to create the budget line.' => 'Impossibile creare il limite al bilancio',
+ 'Unable to remove this budget line.' => 'Impossibile rimuovere questo limite al bilancio.',
+ 'USD - US Dollar' => 'USD - Dollari Americani',
+ 'Remaining' => 'Restanti',
+ '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',
+ 'Source column' => 'Colonna sorgente',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ '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',
+ 'Currency rates' => 'Tassi di cambio',
+ 'Rate' => 'Cambio',
+ 'Change reference currency' => 'Cambia la valuta di riferimento',
+ 'Add a new currency rate' => 'Aggiungi un nuovo tasso di cambio',
+ 'Currency rates are used to calculate project budget.' => 'I tassi di cambio sono utilizzati per calcolare i bilanci dei progetti',
+ 'Reference currency' => 'Valuta di riferimento',
+ '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.',
+ 'Send notifications to a Slack channel' => 'Invia notifiche al canale Slack',
+ 'Webhook URL' => 'URL Webhook',
+ 'Help on Slack integration' => 'Guida all\'integrazione con Slack',
+ '%s remove the assignee of the task %s' => '%s rimuove l\'assegnatario del compito %s',
+ 'Send notifications to Hipchat' => 'Invia notifiche a Hipchat',
+ 'API URL' => 'URL API',
+ 'Room API ID or name' => 'Nome o ID API della Room',
+ 'Room notification token' => 'Token per le notifiche della Room',
+ 'Help on Hipchat integration' => 'Guida all\'integrazione di Hipchat',
+ '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',
+ 'Code' => 'Codice',
+ 'Two factor authentication' => 'Autenticazione a due fattori',
+ 'Enable/disable two factor authentication' => 'Abilita/disabilita autenticazione a due fattori',
+ 'This QR code contains the key URI: ' => 'Questo QR code contiene l\'URI: ',
+ 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salva la chiave privata nel tuo software TOTP (per esempio Google Authenticator oppure FreeOTP).',
+ 'Check my code' => 'Controlla il mio codice',
+ 'Secret key: ' => 'Chiave privata:',
+ 'Test your device' => 'Testa il tuo dispositivo',
+ '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' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
+);
diff --git a/app/Locales/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index 6b6c795d..22dd98ff 100644
--- a/app/Locales/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -1,6 +1,8 @@
<?php
return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
'None' => 'なし',
'edit' => '変更',
'Edit' => '変更',
@@ -182,18 +184,19 @@ return array(
'Change assignee' => '担当を変更する',
'Change assignee for the task "%s"' => 'タスク「%s」の担当を変更する',
'Timezone' => 'タイムゾーン',
- 'Sorry, I didn\'t found this information in my database!' => 'データベース上で情報が見つかりませんでした!',
+ 'Sorry, I didn\'t find this information in my database!' => 'データベース上で情報が見つかりませんでした!',
'Page not found' => 'ページが見つかりません',
'Complexity' => '複雑さ',
'limit' => '制限',
'Task limit' => 'タスク数制限',
+ 'Task count' => 'タスク数',
'This value must be greater than %d' => '%d より大きな値を入力してください',
'Edit project access list' => 'プロジェクトのアクセス許可を変更',
'Edit users access' => 'ユーザのアクセス許可を変更',
'Allow this user' => 'このユーザを許可する',
'Only those users have access to this project:' => 'これらのユーザのみがプロジェクトにアクセスできます:',
'Don\'t forget that administrators have access to everything.' => '管理者には全ての権限が与えられます。',
- 'revoke' => '許可を取り下げる',
+ 'Revoke' => '許可を取り下げる',
'List of authorized users' => '許可されたユーザ',
'User' => 'ユーザ',
'Nobody have access to this project.' => 'だれもプロジェクトにアクセスできません。',
@@ -211,7 +214,8 @@ return array(
'Due Date' => '期限',
'Invalid date' => '日付が無効です',
'Must be done before %B %e, %Y' => '%Y/%m/%d までに完了',
- '%B %e, %Y' => '%d %B %Y',
+ '%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.' => '自動アクションの作成に失敗しました。',
@@ -385,8 +389,6 @@ return array(
'Creator' => '作成者',
'Modification date' => '変更日',
'Completion date' => '完了日',
- 'Webhook URL for task creation' => 'タスク作成の Webhook URL',
- 'Webhook URL for task modification' => 'タスク変更の Webhook URL',
'Clone' => '複製',
'Clone Project' => 'プロジェクトの複製',
'Project cloned successfully.' => 'プロジェクトを複製しました。',
@@ -406,15 +408,13 @@ return array(
'Comment updated' => 'コメントが更新されました',
'New comment posted by %s' => '「%s」の新しいコメントが追加されました',
'List of due tasks for the project "%s"' => 'プロジェクト「%s」の期限切れのタスク',
- '[%s][New attachment] %s (#%d)' => '[%s][新規添付ファイル] %s (#%d)',
- '[%s][New comment] %s (#%d)' => '[%s][新規コメント] %s (#%d)',
- '[%s][Comment updated] %s (#%d)' => '[%s][コメント更新] %s (#%d)',
- '[%s][New subtask] %s (#%d)' => '[%s][新規サブタスク] %s (#%d)',
- '[%s][Subtask updated] %s (#%d)' => '[%s][サブタスク更新] %s (#%d)',
- '[%s][New task] %s (#%d)' => '[%s][新規タスク] %s (#%d)',
- '[%s][Task updated] %s (#%d)' => '[%s][タスク更新] %s (#%d)',
- '[%s][Task closed] %s (#%d)' => '[%s][タスククローズ] %s (#%d)',
- '[%s][Task opened] %s (#%d)' => '[%s][タスクオープン] %s (#%d)',
+ 'New attachment' => '新しい添付ファイル',
+ 'New comment' => '新しいコメント',
+ 'New subtask' => '新しいサブタスク',
+ 'Subtask updated' => 'サブタスクの更新',
+ 'Task updated' => 'タスクの更新',
+ 'Task closed' => 'タスクのクローズ',
+ 'Task opened' => 'タスクのオープン',
'[%s][Due tasks]' => '[%s][タスク期限切れ]',
'[Kanboard] Notification' => '[Kanboard] 通知',
'I want to receive notifications only for those projects:' => '以下のプロジェクトにのみ通知を受け取る:',
@@ -449,7 +449,7 @@ return array(
'Email:' => 'Email:',
'Default project:' => 'デフォルトプロジェクト:',
'Notifications:' => '通知:',
- // 'Notifications' => '',
+ 'Notifications' => '通知',
'Group:' => 'グループ:',
'Regular user' => '通常のユーザ',
'Account type:' => 'アカウントの種類:',
@@ -467,18 +467,18 @@ return array(
'Unable to change the password.' => 'パスワードが変更できませんでした。',
'Change category for the task "%s"' => 'タスク「%s」のカテゴリの変更',
'Change category' => 'カテゴリの変更',
- '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> をアップデートしました',
- '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> をオープンしました',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> をポジション #%d カラム %s に移動しました',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> をカラム「%s」に移動しました',
- '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> を作成しました',
- '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> をクローズしました',
- '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> のサブタスクを追加しました',
- '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> のサブタスクを更新しました',
+ '%s updated the task %s' => '%s がタスク %s をアップデートしました',
+ '%s opened the task %s' => '%s がタスク %s をオープンしました',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s がタスク %s をポジション #%d カラム %s に移動しました',
+ '%s moved the task %s to the column "%s"' => '%s がタスク %s をカラム「%s」に移動しました',
+ '%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 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> のコメントを更新しました',
- '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> にコメントしました',
+ '%s updated a comment on the task %s' => '%s がタスク %s のコメントを更新しました',
+ '%s commented the task %s' => '%s がタスク %s にコメントしました',
'%s\'s activity' => '%s のアクティビティ',
'No activity.' => 'アクティビティなし。',
'RSS feed' => 'RSS フィード',
@@ -497,10 +497,10 @@ return array(
'Default columns for new projects (Comma-separated)' => '新規プロジェクトのデフォルトカラム (コンマで区切って入力)',
'Task assignee change' => '担当者の変更',
'%s change the assignee of the task #%d to %s' => '%s がタスク #%d の担当を %s に変更しました',
- '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '%s がタスク <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> の担当を %s に変更しました',
- '[%s][Column Change] %s (#%d)' => '[%s][カラムの変更] %s (#%d)',
- '[%s][Position Change] %s (#%d)' => '[%s][位置の変更] %s (#%d)',
- '[%s][Assignee Change] %s (#%d)' => '[%s][担当者変更] %s (#%d)',
+ '%s changed the assignee of the task %s to %s' => '%s がタスク %s の担当を %s に変更しました',
+ 'Column Change' => 'カラムの変更',
+ 'Position Change' => '位置の変更',
+ 'Assignee Change' => '担当の変更',
'New password for the user "%s"' => 'ユーザ「%s」の新しいパスワード',
'Choose an event' => 'イベントの選択',
'Github commit received' => 'Github のコミットを受け取った',
@@ -544,11 +544,382 @@ return array(
'Started on %B %e, %Y' => '開始 %Y/%m/%d',
'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.' => '',
+ 'There is nothing assigned to you.' => '何もアサインされていません。',
+ 'My tasks' => '自分のタスク',
+ 'Activity stream' => 'アクティビティストリーム',
+ 'Dashboard' => 'ダッシュボード',
+ 'Confirmation' => '確認',
+ 'Allow everybody to access to this project' => '全員にプロジェクトへのアクセスを許す',
+ 'Everybody have access to this project.' => '誰でもこのプロジェクトにアクセスできます。',
+ 'Webhooks' => 'Webhook',
+ 'API' => 'API',
+ 'Integration' => '連携',
+ 'Github webhooks' => 'Github Webhook',
+ 'Help on Github webhooks' => 'Github webhook のヘルプ',
+ 'Create a comment from an external provider' => '外部サービスからコメントを作成する',
+ 'Github issue comment created' => 'Github Issue コメントが作られました',
+ 'Configure' => '設定',
+ '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.' => 'カラムを削除しました',
+ 'Edit Project' => 'プロジェクトを編集する',
+ 'Github Issue' => 'Github Issue',
+ 'Not enough data to show the graph.' => 'グラフを描画するには出たが足りません',
+ 'Previous' => '戻る',
+ 'The id must be an integer' => 'id は数字でなければなりません',
+ 'The project id must be an integer' => 'project id は数字でなければなりません',
+ 'The status must be an integer' => 'status は数字でなければなりません',
+ 'The subtask id is required' => 'subtask id が必要です',
+ 'The subtask id must be an integer' => 'subtask id は数字でなければなりません',
+ 'The task id is required' => 'task id が必要です',
+ 'The task id must be an integer' => 'task id は数字でなければなりません',
+ 'The user id must be an integer' => 'user id は数字でなければなりません',
+ 'This value is required' => 'この値が必要です',
+ 'This value must be numeric' => 'この値は数字でなければなりません',
+ 'Unable to create this task.' => 'このタスクを作成できませんでした',
+ 'Cumulative flow diagram' => '蓄積フロー図',
+ 'Cumulative flow diagram for "%s"' => '「%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' => '書く',
+ '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.' => 'スイムレーンが作成されました。',
+ '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' => 'サブタスクの出力',
+ 'Subtasks exportation for "%s"' => '「%s」のサブタスク出力',
+ 'Task Title' => 'タスクタイトル',
+ 'Untitled' => 'タイトル無し',
+ 'Application default' => 'アプリケーションデフォルト',
+ 'Language:' => '言語:',
+ 'Timezone:' => 'タイムゾーン:',
+ 'All columns' => '全てのカラム',
+ 'Calendar for "%s"' => '「%s」のカレンダー',
+ 'Filter by column' => 'カラムでフィルタ',
+ 'Filter by status' => 'ステータスでフィルタ',
+ 'Calendar' => 'カレンダー',
+ 'Next' => '次へ',
+ '#%d' => '#%d',
+ 'Filter by color' => '色でフィルタ',
+ 'Filter by swimlane' => 'スイムレーンでフィルタ',
+ 'All swimlanes' => '全てのスイムレーン',
+ 'All colors' => '全ての色',
+ 'All status' => '全てのステータス',
+ 'Add a comment logging moving the task between columns' => 'カラム間のタスク移動をコメントに記録',
+ '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」の編集',
+ 'Enable time tracking for subtasks' => 'サブタスクのタイムトラッキングを有効',
+ '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?' => 'プロジェクトの何を複製しますか?',
+ 'Change dashboard view' => 'ダッシュボードビューを変更',
+ 'Show/hide activities' => 'アクティビティの表示・非表示',
+ 'Show/hide projects' => 'プロジェクトの表示・非表示',
+ 'Show/hide subtasks' => 'サブタスクの表示・非表示',
+ 'Show/hide tasks' => 'タスクの表示・非表示',
+ 'Disable login form' => 'ログインフォームの無効化',
+ 'Show/hide calendar' => 'カレンダーの表示・非表示',
+ 'User calendar' => 'ユーザカレンダー',
+ 'Bitbucket commit received' => 'Bitbucket コミットを受信しました',
+ 'Bitbucket webhooks' => 'Bitbucket Webhooks',
+ 'Help on Bitbucket webhooks' => 'Bitbucket Webhooks のヘルプ',
+ 'Start' => '開始',
+ 'End' => '終了',
+ 'Task age in days' => 'タスクの経過日数',
+ 'Days in this column' => 'カラムでの経過日数',
+ '%dd' => '%d 日',
+ 'Add a link' => 'リンクの追加',
+ 'Add a new link' => '新しいリンクの追加',
+ 'Do you really want to remove this link: "%s"?' => 'リンク「%s」を本当に削除しますか?',
+ 'Do you really want to remove this link with task #%d?' => 'このリンクとタスク#%dを削除しますか?',
+ 'Field required' => 'フィールドが必要です',
+ 'Link added successfully.' => 'リンクを追加しました。',
+ 'Link updated successfully.' => 'リンクを更新しました。',
+ 'Link removed successfully.' => 'リンクを削除しました。',
+ 'Link labels' => 'リンクラベル',
+ 'Link modification' => 'リンクの変更',
+ 'Links' => 'リンク',
+ 'Link settings' => 'リンク設定',
+ 'Opposite label' => '反対のラベル',
+ 'Remove a link' => 'ラベルの削除',
+ 'Task\'s links' => 'タスクのラベル',
+ 'The labels must be different' => '異なるラベルを指定してください',
+ 'There is no link.' => 'リンクがありません',
+ 'This label must be unique' => 'ラベルはユニークである必要があります',
+ 'Unable to create your link.' => 'リンクを作成できませんでした。',
+ 'Unable to update your link.' => 'リンクを更新できませんでした。',
+ 'Unable to remove this link.' => 'リンクを削除できませんでした。',
+ 'relates to' => '次に関連します',
+ 'blocks' => '次をブロックしています',
+ 'is blocked by' => '次にブロックされています',
+ 'duplicates' => '次に重複しています',
+ 'is duplicated by' => '次に重複しています',
+ 'is a child of' => '次の子タスクです ',
+ 'is a parent of' => '次の親タスクです',
+ 'targets milestone' => '次のマイルストーンを目標とします',
+ 'is a milestone of' => '次のタスクのマイルストーンです',
+ 'fixes' => '次を修正します',
+ 'is fixed by' => '次に修正されます',
+ 'This task' => 'このタスクは',
+ '<1h' => '<1時間',
+ '%dh' => '%d 時間',
+ '%b %e' => '%b/%e',
+ 'Expand tasks' => 'タスクを展開する',
+ 'Collapse tasks' => 'タスクを閉じる',
+ 'Expand/collapse tasks' => 'タスクの展開/閉じる',
+ 'Close dialog box' => 'ダイアログボックスを閉じる',
+ 'Submit a form' => 'フォームを送信する',
+ 'Board view' => 'ボードビュー',
+ 'Keyboard shortcuts' => 'キーボードショートカット',
+ 'Open board switcher' => 'ボード切り替えを開く',
+ 'Application' => 'アプリケーション',
+ 'Filter recently updated' => 'フィルタがアップデートされました',
+ 'since %B %e, %Y at %k:%M %p' => '%Y/%m/%d %k:%M から',
+ 'More filters' => '他のフィルタ',
+ 'Compact view' => 'コンパクトビュー',
+ 'Horizontal scrolling' => '縦スクロール',
+ 'Compact/wide view' => 'コンパクト/ワイドビュー',
+ 'No results match:' => '結果が一致しませんでした',
+ 'Remove hourly rate' => '毎時レートを削除',
+ 'Do you really want to remove this hourly rate?' => '毎時レートを削除しますか?',
+ 'Hourly rates' => '毎時レート',
+ 'Hourly rate' => '毎時レート',
+ 'Currency' => '通貨',
+ 'Effective date' => '有効期限',
+ 'Add new rate' => '新しいレート',
+ 'Rate removed successfully.' => 'レートの削除に成功しました。',
+ 'Unable to remove this rate.' => 'レートを削除できませんでした。',
+ 'Unable to save the hourly rate.' => '時間毎のレートを保存できませんでした。',
+ 'Hourly rate created successfully.' => '時間毎のレートを作成しました。',
+ 'Start time' => '開始時間',
+ 'End time' => '終了時間',
+ 'Comment' => 'コメント',
+ 'All day' => '終日',
+ 'Day' => '日',
+ 'Manage timetable' => 'タイムテーブルの管理',
+ 'Overtime timetable' => '残業タイムテーブル',
+ 'Time off timetable' => '休暇タイムテーブル',
+ 'Timetable' => 'タイムテーブル',
+ 'Work timetable' => 'ワークタイムテーブル',
+ 'Week timetable' => '週次タイムテーブル',
+ 'Day timetable' => '日時タイムテーブル',
+ 'From' => 'ここから',
+ 'To' => 'ここまで',
+ // 'Time slot created successfully.' => '',
+ // 'Unable to save this time slot.' => '',
+ // 'Time slot removed successfully.' => '',
+ // 'Unable to remove this time slot.' => '',
+ // 'Do you really want to remove this time slot?' => '',
+ 'Remove time slot' => 'タイムスロットの削除',
+ 'Add new time slot' => 'タイムラインの追加',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'このタイムテーブルは、残業や休暇で全日がチェックされた場合に用いられます。',
+ 'Files' => 'ファイル',
+ 'Images' => '画像',
+ 'Private project' => 'プライベートプロジェクト',
+ 'Amount' => '数量',
+ 'AUD - Australian Dollar' => 'AUD - 豪ドル',
+ 'Budget' => '予算',
+ 'Budget line' => '予算ライン',
+ 'Budget line removed successfully.' => '予算ラインを削除しました.',
+ 'Budget lines' => '予算ライン',
+ 'CAD - Canadian Dollar' => 'CAD - 加ドル',
+ 'CHF - Swiss Francs' => 'CHF - スイスフラン',
+ 'Cost' => 'コスト',
+ 'Cost breakdown' => 'コストブレークダウン',
+ 'Custom Stylesheet' => 'カスタムスタイルシート',
+ 'download' => 'ダウンロード',
+ 'Do you really want to remove this budget line?' => 'この予算ラインを本当に削除しますか?',
+ 'EUR - Euro' => 'EUR - ユーロ',
+ 'Expenses' => '支出',
+ 'GBP - British Pound' => 'GBP - 独ポンド',
+ 'INR - Indian Rupee' => 'INR - 伊ルピー',
+ 'JPY - Japanese Yen' => 'JPY - 日本円',
+ 'New budget line' => '新しい予算ライン',
+ 'NZD - New Zealand Dollar' => 'NZD - NZ ドル',
+ 'Remove a budget line' => '予算ラインの削除',
+ 'Remove budget line' => '予算ラインの削除',
+ 'RSD - Serbian dinar' => 'RSD - セルビアデナール',
+ 'The budget line have been created successfully.' => '予算ラインを作成しました',
+ 'Unable to create the budget line.' => '予算ラインを作成できませんでした。',
+ 'Unable to remove this budget line.' => '予算ラインを削除できませんでした。',
+ 'USD - US Dollar' => 'USD - 米ドル',
+ 'Remaining' => '残り',
+ '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' => '移動元のカラム',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ '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' => '新しい通貨レートを追加',
+ 'Currency rates are used to calculate project budget.' => '通貨レートはプロジェクト予算の算出に利用されます。',
+ 'Reference currency' => '基軸通貨',
+ // 'The currency rate have been added successfully.' => '',
+ 'Unable to add this currency rate.' => 'この通貨レートを追加できません。',
+ 'Send notifications to a Slack channel' => 'Slack チャンネルに通知を送信',
+ 'Webhook URL' => 'Webhook URL',
+ 'Help on Slack integration' => 'Slack 連携のヘルプ',
+ '%s remove the assignee of the task %s' => '%s がタスク「%s」の担当を解除しました。',
+ 'Send notifications to Hipchat' => 'Hipchat に通知を送信',
+ 'API URL' => 'API URL',
+ 'Room API ID or name' => 'Room API ID または名前',
+ 'Room notification token' => 'Room 通知トークン',
+ 'Help on Hipchat integration' => 'Hipchat 連携のヘルプ',
+ 'Enable Gravatar images' => 'Gravatar イメージを有効化',
+ 'Information' => '情報 ',
+ 'Check two factor authentication code' => '2 段認証をチェックする',
+ 'The two factor authentication code is not valid.' => '2 段認証コードは無効です。',
+ 'The two factor authentication code is valid.' => '2 段認証コードは有効です。',
+ 'Code' => 'コード',
+ 'Two factor authentication' => '2 段認証',
+ '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).' => '',
+ // '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' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
new file mode 100644
index 00000000..43ed7e35
--- /dev/null
+++ b/app/Locale/nl_NL/translations.php
@@ -0,0 +1,925 @@
+<?php
+
+return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
+ 'None' => 'Geen',
+ 'edit' => 'bewerken',
+ 'Edit' => 'Bewerken',
+ 'remove' => 'verwijderen',
+ 'Remove' => 'Verwijderen',
+ 'Update' => 'Update',
+ 'Yes' => 'Ja',
+ 'No' => 'Nee',
+ 'cancel' => 'annuleren',
+ 'or' => 'of',
+ 'Yellow' => 'Geel',
+ 'Blue' => 'Blauw',
+ 'Green' => 'Groen',
+ 'Purple' => 'Paars',
+ 'Red' => 'Rood',
+ 'Orange' => 'Oranje',
+ 'Grey' => 'Grijs',
+ 'Save' => 'Opslaan',
+ 'Login' => 'Inloggen',
+ 'Official website:' => 'Officiële website :',
+ 'Unassigned' => 'Niet toegewezen',
+ 'View this task' => 'Deze taak bekijken',
+ 'Remove user' => 'Gebruiker verwijderen',
+ 'Do you really want to remove this user: "%s"?' => 'Weet u zeker dat u deze gebruiker wil verwijderen : « %s » ?',
+ 'New user' => 'Nieuwe gebruiker',
+ 'All users' => 'Alle gebruikers',
+ 'Username' => 'Gebruikersnaam',
+ 'Password' => 'Wachtwoord',
+ 'Default project' => 'Standaard wachtwoord',
+ 'Administrator' => 'Administrator',
+ 'Sign in' => 'Inloggen',
+ 'Users' => 'Gebruikers',
+ 'No user' => 'Geen gebruiker',
+ 'Forbidden' => 'Geweigerd',
+ 'Access Forbidden' => 'Toegang geweigerd',
+ 'Only administrators can access to this page.' => 'Alleen administrators hebben toegang tot deze pagina.',
+ 'Edit user' => 'Gebruiker bewerken',
+ 'Logout' => 'Uitloggen',
+ 'Bad username or password' => 'Verkeerde gebruikersnaam of wachtwoord',
+ 'users' => 'gebruikers',
+ 'projects' => 'projecten',
+ 'Edit project' => 'Project bewerken',
+ 'Name' => 'Naam',
+ 'Activated' => 'Geactiveerd',
+ 'Projects' => 'Projecten',
+ 'No project' => 'Geen project',
+ 'Project' => 'Project',
+ 'Status' => 'Status',
+ 'Tasks' => 'Taken',
+ 'Board' => 'Bord',
+ 'Actions' => 'Acties',
+ 'Inactive' => 'Inactief',
+ 'Active' => 'Actief',
+ 'Column %d' => 'Kolom %d',
+ 'Add this column' => 'Deze kolom toevoegen',
+ '%d tasks on the board' => '%d taken op het bord',
+ '%d tasks in total' => '%d taken in totaal',
+ 'Unable to update this board.' => 'Update van dit bord niet mogelijk.',
+ 'Edit board' => 'Bord bewerken',
+ 'Disable' => 'Deactiveren',
+ 'Enable' => 'Activeren',
+ 'New project' => 'Nieuw project',
+ 'Do you really want to remove this project: "%s"?' => 'Weet u zeker dat u dit project wil verwijderen : « %s » ?',
+ 'Remove project' => 'Project verwijderen',
+ 'Boards' => 'Borden',
+ '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',
+ 'Add Column' => 'Kolom toevoegen',
+ 'Project "%s"' => 'Project « %s »',
+ 'Nobody assigned' => 'Niemand toegewezen',
+ 'Assigned to %s' => 'Toegewezen aan %s',
+ 'Remove a column' => 'Kolom verwijderen',
+ 'Remove a column from a board' => 'Kolom verwijderen van het bord',
+ 'Unable to remove this column.' => 'Verwijderen van deze kolom niet mogelijk.',
+ 'Do you really want to remove this column: "%s"?' => 'Weet u zeker dat u deze kolom wil verwijderen : « %s » ?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Deze actie zal ALLE TAKEN VERWIJDEREN die zijn geassocieerd met deze kolom!',
+ 'Settings' => 'Instellingen',
+ 'Application settings' => 'Applicatie instellingen',
+ 'Language' => 'Taal',
+ 'Webhook token:' => 'Webhook token :',
+ 'API token:' => 'API token :',
+ 'More information' => 'Meer informatie',
+ 'Database size:' => 'Database grootte :',
+ 'Download the database' => 'Download de database',
+ 'Optimize the database' => 'Optimaliseer de database',
+ '(VACUUM command)' => '(VACUUM commando)',
+ '(Gzip compressed Sqlite file)' => '(Gzip ingepakt Sqlite bestand)',
+ 'User settings' => 'Gebruikers instellingen',
+ 'My default project:' => 'Mijn standaard project : ',
+ 'Close a task' => 'Taak sluiten',
+ 'Do you really want to close this task: "%s"?' => 'Weet u zeker dat u deze taak wil sluiten : « %s » ?',
+ 'Edit a task' => 'Taak bewerken',
+ 'Column' => 'Kolom',
+ 'Color' => 'Kleur',
+ 'Assignee' => 'Toegewezene',
+ 'Create another task' => 'Nog een taak aanmaken',
+ 'New task' => 'Nieuwe taak',
+ '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.',
+ 'Add a new task' => 'Een nieuwe taak toevoegen',
+ 'The username is required' => 'De gebruikersnaam is verplicht',
+ 'The maximum length is %d characters' => 'De maximale lengte is %d karakters',
+ 'The minimum length is %d characters' => 'De minimale lengte is %d karakters',
+ 'The password is required' => 'Het wachtwoord is verplicht',
+ 'This value must be an integer' => 'Deze waarde dient een integer te zijn',
+ 'The username must be unique' => 'De gebruikersnaam moet uniek zijn',
+ 'The username must be alphanumeric' => 'De gebruikersnaam moet alfanumeriek zijn',
+ 'The user id is required' => 'Het gebruikers id is verplicht',
+ 'Passwords don\'t match' => 'De wachtwoorden komen niet overeen',
+ 'The confirmation is required' => 'De bevestiging is verplicht',
+ 'The column is required' => 'De kolom is verplicht',
+ 'The project is required' => 'Het project is verplicht',
+ 'The color is required' => 'De kleur is verplicht',
+ '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',
+ 'The language is required' => 'De taal is verplicht',
+ 'There is no active project, the first step is to create a new project.' => 'Er is geen actief project, de eerste stap is een nieuw project aanmaken.',
+ 'Settings saved successfully.' => 'Instellingen succesvol opgeslagen.',
+ 'Unable to save your settings.' => 'Instellingen opslaan niet gelukt.',
+ 'Database optimization done.' => 'Database optimaliseren voltooid.',
+ 'Your project have been created successfully.' => 'Uw project is succesvol aangemaakt.',
+ 'Unable to create your project.' => 'Het aanmaken van het project is niet gelukt.',
+ 'Project updated successfully.' => 'Project succesvol geupdate.',
+ 'Unable to update this project.' => 'Updaten van project niet gelukt.',
+ 'Unable to remove this project.' => 'Verwijderen van project niet gelukt.',
+ 'Project removed successfully.' => 'Project succesvol verwijderd.',
+ 'Project activated successfully.' => 'Project succesvol geactiveerd.',
+ 'Unable to activate this project.' => 'Project activeren niet gelukt.',
+ 'Project disabled successfully.' => 'Project uitschakelen succesvol.',
+ 'Unable to disable this project.' => 'Project uitschakelen niet gelukt.',
+ 'Unable to open this task.' => 'Openen van deze taak niet gelukt.',
+ 'Task opened successfully.' => 'Taak succesvol geopend.',
+ 'Unable to close this task.' => 'Sluiten van deze taak niet gelukt.',
+ 'Task closed successfully.' => 'Taak succesvol gesloten.',
+ 'Unable to update your task.' => 'Updaten van uw taak mislukt.',
+ 'Task updated successfully.' => 'Taak succesvol geupdate.',
+ 'Unable to create your task.' => 'Taak aanmaken niet gelukt.',
+ 'Task created successfully.' => 'Taak succesvol aangemaakt.',
+ 'User created successfully.' => 'Gebruiker succesvol aangemaakt.',
+ 'Unable to create your user.' => 'Aanmaken van gebruiker niet gelukt.',
+ 'User updated successfully.' => 'Gebruiker succesvol geupdate',
+ 'Unable to update your user.' => 'Updaten van gebruiker niet gelukt.',
+ 'User removed successfully.' => 'Gebruiker succesvol verwijderd.',
+ 'Unable to remove this user.' => 'Verwijderen van gebruikers niet gelukt.',
+ 'Board updated successfully.' => 'Board succesvol geupdate.',
+ 'Ready' => 'Klaar',
+ 'Backlog' => 'Het wachten',
+ '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',
+ 'No task' => 'Geen taak',
+ 'Completed tasks' => 'Voltooide taken',
+ 'List of projects' => 'Lijst van projecten',
+ 'Completed tasks for "%s"' => 'Vooltooide taken voor « %s »',
+ '%d closed tasks' => '%d gesloten taken',
+ 'No task for this project' => 'Geen taken voor dit project',
+ 'Public link' => 'Publieke link',
+ 'There is no column in your project!' => 'Er is geen kolom in uw project !',
+ 'Change assignee' => 'Toegewezene aanpassen',
+ 'Change assignee for the task "%s"' => 'Toegewezene aanpassen voor taak « %s »',
+ 'Timezone' => 'Tijdzone',
+ 'Sorry, I didn\'t find this information in my database!' => 'Sorry deze informatie kon niet worden gevonden in de database !',
+ 'Page not found' => 'Pagina niet gevonden',
+ 'Complexity' => 'Complexiteit',
+ 'limit' => 'Limiet',
+ 'Task limit' => 'Taak limiet.',
+ 'Task count' => 'Aantal taken',
+ 'This value must be greater than %d' => 'Deze waarde moet groter zijn dan %d',
+ 'Edit project access list' => 'Aanpassen toegangsrechten project',
+ 'Edit users access' => 'Gebruikerstoegang aanpassen',
+ 'Allow this user' => 'Deze gebruiker toestaan',
+ 'Only those users have access to this project:' => 'Alleen deze gebruikers hebben toegang tot dit project :',
+ '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',
+ 'You are not allowed to access to this project.' => 'U heeft geen toegang tot dit project.',
+ 'Comments' => 'Commentaar',
+ 'Post comment' => 'Commentaar toevoegen',
+ 'Write your text in Markdown' => 'Schrijf uw tekst in Markdown',
+ 'Leave a comment' => 'Schrijf een commentaar',
+ 'Comment is required' => 'Commentaar is verplicht',
+ 'Leave a description' => 'Schrijf een omschrijving',
+ 'Comment added successfully.' => 'Commentaar succesvol toegevoegd.',
+ 'Unable to create your comment.' => 'Commentaar toevoegen niet gelukt.',
+ 'The description is required' => 'Omschrijving is verplicht',
+ '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.',
+ 'Remove an action' => 'Actie verwijderen',
+ 'Unable to remove this action.' => 'Actie verwijderen niet gelukt',
+ 'Action removed successfully.' => 'Actie succesvol verwijder.',
+ 'Automatic actions for the project "%s"' => 'Automatiseer acties voor project « %s »',
+ 'Defined actions' => 'Gedefinieerde acties',
+ 'Add an action' => 'Actie toevoegen',
+ 'Event name' => 'Naam gebeurtenis',
+ 'Action name' => 'Actie naam',
+ 'Action parameters' => 'Actie paramaters',
+ 'Action' => 'Actie',
+ 'Event' => 'Evenement',
+ 'When the selected event occurs execute the corresponding action.' => 'Als de geselecteerde gebeurtenis optreedt de volgende actie uitvoeren',
+ 'Next step' => 'Volgende stap',
+ 'Define action parameters' => 'Bepaal actie parameters',
+ 'Save this action' => 'Actie opslaan',
+ 'Do you really want to remove this action: "%s"?' => 'Weet u zeker dat u de volgende actie wil verwijderen : « %s » ?',
+ 'Remove an automatic action' => 'Automatische actie verwijderen',
+ 'Close the task' => 'Taak sluiten',
+ 'Assign the task to a specific user' => 'Taak toewijzen aan een gebruiker',
+ 'Assign the task to the person who does the action' => 'Taak toewijzen aan een gebruiker die de actie uitvoert',
+ 'Duplicate the task to another project' => 'Taak dupliceren in een ander project',
+ 'Move a task to another column' => 'Taak verplaatsen naar een andere kolom',
+ 'Move a task to another position in the same column' => 'Taak verplaatsen naar een andere positie in dezelfde kolom',
+ 'Task modification' => 'Taak aanpassen',
+ 'Task creation' => 'Taak aanmaken',
+ 'Open a closed task' => 'Gesloten taak openen',
+ 'Closing a task' => 'Taak sluiten',
+ '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',
+ 'Update this comment' => 'Commentaar aanpassen',
+ 'Comment updated successfully.' => 'Commentaar succesvol aangepast.',
+ 'Unable to update your comment.' => 'Commentaar aanpassen niet gelukt.',
+ 'Remove a comment' => 'Commentaar verwijderen',
+ '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.',
+ 'Details' => 'Details',
+ 'Current password for the user "%s"' => 'Huidig wachtwoord voor gebruiker « %s »',
+ 'The current password is required' => 'Huidig wachtwoord is verplicht',
+ 'Wrong password' => 'Onjuist wachtwoord',
+ 'Reset all tokens' => 'Alle tokens resetten',
+ 'All tokens have been regenerated.' => 'Alle tokens zijn opnieuw gegenereerd.',
+ 'Unknown' => 'Onbekend',
+ 'Last logins' => 'Laatste logins',
+ 'Login date' => 'Login datum',
+ 'Authentication method' => 'Authenticatie methode',
+ 'IP address' => 'IP adres',
+ 'User agent' => 'User agent',
+ 'Persistent connections' => 'Persistente connectie',
+ 'No session.' => 'Geen sessie.',
+ 'Expiration date' => 'Verloopdatum',
+ 'Remember Me' => 'Onthoud mij',
+ 'Creation date' => 'Aanmaakdatum',
+ 'Filter by user' => 'Filter op gebruiker',
+ 'Filter by due date' => 'Filter op vervaldatum',
+ 'Everybody' => 'Iedereen',
+ 'Open' => 'Open',
+ 'Closed' => 'Gesloten',
+ 'Search' => 'Zoek',
+ 'Nothing found.' => 'Niets gevonden.',
+ 'Search in the project "%s"' => 'Zoek in project « %s »',
+ 'Due date' => 'Vervaldatum',
+ 'Others formats accepted: %s and %s' => 'Andere toegestane formaten : %s en %s',
+ 'Description' => 'Omschrijving',
+ '%d comments' => '%d commentaren',
+ '%d comment' => '%d commentaar',
+ 'Email address invalid' => 'Ongeldig emailadres',
+ 'Your Google Account is not linked anymore to your profile.' => 'Uw Google Account is niet meer aan uw profiel gelinkt.',
+ 'Unable to unlink your Google Account.' => 'Verwijderen link met Google Account niet gelukt.',
+ 'Google authentication failed' => 'Google authenticatie niet gelukt',
+ 'Unable to link your Google Account.' => 'Linken met Google Account niet gelukt',
+ 'Your Google Account is linked to your profile successfully.' => 'Linken met Google Account succesvol.',
+ '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 #%d' => 'Taak %d',
+ 'Task removed successfully.' => 'Taak succesvol verwijderd.',
+ 'Unable to remove this task.' => 'Taak verwijderen niet gelukt.',
+ 'Remove a task' => 'Taak verwijderen',
+ 'Do you really want to remove this task: "%s"?' => 'Weet u zeker dat u deze taak wil verwijderen « %s » ?',
+ 'Assign automatically a color based on a category' => 'Automatisch een kleur toewijzen aan de hand van een categorie',
+ 'Assign automatically a category based on a color' => 'Automatisch een categorie toewijzen aan de hand van een kleur',
+ 'Task creation or modification' => 'Taak aanmaken of wijzigen',
+ 'Category' => 'Categorie',
+ 'Category:' => 'Categorie :',
+ 'Categories' => 'Categorieën',
+ 'Category not found.' => 'Categorie niet gevonden',
+ 'Your category have been created successfully.' => 'Categorie succesvol aangemaakt.',
+ 'Unable to create your category.' => 'Categorie aanmaken niet gelukt.',
+ 'Your category have been updated successfully.' => 'Categorie succesvol aangepast.',
+ 'Unable to update your category.' => 'Aanpassen van categorie niet gelukt.',
+ 'Remove a category' => 'Categorie verwijderen',
+ 'Category removed successfully.' => 'Categorie succesvol verwijderd.',
+ 'Unable to remove this category.' => 'Categorie verwijderen niet gelukt.',
+ 'Category modification for the project "%s"' => 'Categorie aanpassen voor project « %s »',
+ 'Category Name' => 'Categorie naam',
+ 'Categories for the project "%s"' => 'Categorieën voor project « %s »',
+ 'Add a new category' => 'Categorie toevoegen',
+ 'Do you really want to remove this category: "%s"?' => 'Weet u zeker dat u deze categorie wil verwijderen: « %s » ?',
+ 'Filter by category' => 'Filter op categorie',
+ 'All categories' => 'Alle categorieën',
+ 'No category' => 'Geen categorie',
+ 'The name is required' => 'De naam is verplicht',
+ 'Remove a file' => 'Bestand verwijderen',
+ 'Unable to remove this file.' => 'Bestand verwijderen niet gelukt.',
+ 'File removed successfully.' => 'Bestand succesvol verwijdered.',
+ 'Attach a document' => 'Document toevoegen',
+ 'Do you really want to remove this file: "%s"?' => 'Weet u zeker dat u dit bestand wil verwijderen: « %s » ?',
+ 'open' => 'openen',
+ 'Attachments' => 'Bijlages',
+ 'Edit the task' => 'Taak aanpassen',
+ 'Edit the description' => 'Omschrijving aanpassen',
+ 'Add a comment' => 'Commentaar toevoegen',
+ 'Edit a comment' => 'Commentaar aanpassen',
+ 'Summary' => 'Samenvatting',
+ 'Time tracking' => 'Tijdschrijven',
+ 'Estimate:' => 'Schatting :',
+ 'Spent:' => 'Besteed :',
+ 'Do you really want to remove this sub-task?' => 'Weet u zeker dat u deze subtaak wil verwijderen ?',
+ 'Remaining:' => 'Restant :',
+ 'hours' => 'uren',
+ 'spent' => 'besteed',
+ 'estimated' => 'geschat',
+ 'Sub-Tasks' => 'Subtaken',
+ 'Add a sub-task' => 'Subtaak toevoegen',
+ 'Original estimate' => 'Orginele schatting',
+ 'Create another sub-task' => 'Nog een subtaak toevoegen',
+ 'Time spent' => 'Tijd besteed',
+ 'Edit a sub-task' => 'Subtaak aanpassen',
+ 'Remove a sub-task' => 'Subtaak verwijderen',
+ 'The time must be a numeric value' => 'De tijd moet een numerieke waarde zijn',
+ 'Todo' => 'Nog te doen',
+ 'In progress' => 'In behandeling',
+ 'Sub-task removed successfully.' => 'Subtaak succesvol verwijderd.',
+ 'Unable to remove this sub-task.' => 'Subtaak verwijderen niet gelukt.',
+ 'Sub-task updated successfully.' => 'Subtaak succesvol aangepast.',
+ 'Unable to update your sub-task.' => 'Subtaak aanpassen niet gelukt.',
+ 'Unable to create your sub-task.' => 'Subtaak aanmaken niet gelukt.',
+ 'Sub-task added successfully.' => 'Subtaak succesvol aangemaakt.',
+ 'Maximum size: ' => 'Maximale grootte : ',
+ 'Unable to upload the file.' => 'Uploaden van bestand niet gelukt.',
+ 'Display another project' => 'Een ander project weergeven',
+ 'Your GitHub account was successfully linked to your profile.' => 'Uw Github Account is succesvol gelinkt aan uw profiel.',
+ 'Unable to link your GitHub Account.' => 'Linken van uw Github Account niet gelukt.',
+ 'GitHub authentication failed' => 'Github Authenticatie niet gelukt',
+ 'Your GitHub account is no longer linked to your profile.' => 'Uw Github Account is niet langer gelinkt aan uw profiel.',
+ 'Unable to unlink your GitHub Account.' => 'Verwijdern van de link met uw Github Account niet gelukt.',
+ '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',
+ 'End Date' => 'Einddatum',
+ 'Execute' => 'Uitvoeren',
+ 'Task Id' => 'Taak Id',
+ 'Creator' => 'Aangemaakt door',
+ 'Modification date' => 'Wijzigingsdatum',
+ 'Completion date' => 'Afgerond op',
+ 'Clone' => 'Kloon',
+ 'Clone Project' => 'Project klonen',
+ 'Project cloned successfully.' => 'Project succesvol gekloond.',
+ 'Unable to clone this project.' => 'Klonen van project niet gelukt.',
+ 'Email notifications' => 'Email notificatie',
+ 'Enable email notifications' => 'Email notificatie aanzetten',
+ 'Task position:' => 'Taak positie :',
+ 'The task #%d have been opened.' => 'Taak #%d is geopend.',
+ 'The task #%d have been closed.' => 'Taak #%d is gesloten.',
+ 'Sub-task updated' => 'Subtaak aangepast',
+ 'Title:' => 'Titel :',
+ 'Status:' => 'Status :',
+ 'Assignee:' => 'Toegewezene :',
+ 'Time tracking:' => 'Tijdschrijven :',
+ 'New sub-task' => 'Nieuwe subtaak',
+ 'New attachment added "%s"' => 'Nieuwe bijlage toegevoegd « %s »',
+ 'Comment updated' => 'Commentaar aangepast',
+ 'New comment posted by %s' => 'Nieuw commentaar geplaatst door « %s »',
+ 'List of due tasks for the project "%s"' => 'Lijst van taken die binnenkort voltooid moeten worden voor project « %s »',
+ 'New attachment' => 'Nieuwe bijlage',
+ 'New comment' => 'Nieuw commentaar',
+ 'New subtask' => 'Nieuwe subtaak',
+ 'Subtask updated' => 'Subtaak aangepast',
+ 'Task updated' => 'Taak aangepast',
+ 'Task closed' => 'Taak gesloten',
+ 'Task opened' => 'Taak geopend',
+ '[%s][Due tasks]' => '[%s][binnekort te voltooien taken]',
+ '[Kanboard] Notification' => '[Kanboard] Notificatie',
+ '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',
+ 'Category management' => 'Categorie management',
+ 'User management' => 'Gebruikers management',
+ 'Active tasks' => 'Actieve taken',
+ 'Disable public access' => 'Publieke toegang uitschakelen',
+ 'Enable public access' => 'Publieke toegang inschakelen',
+ 'Active projects' => 'Actieve projecten',
+ 'Inactive projects' => 'Inactieve projecten',
+ 'Public access disabled' => 'Publieke toegang uitgeschakeld',
+ 'Do you really want to disable this project: "%s"?' => 'Weet u zeker dat u dit project wil uitschakelen : « %s » ?',
+ 'Do you really want to duplicate this project: "%s"?' => 'Weet u zeker dat u dit project wil dupliceren : « %s » ?',
+ 'Do you really want to enable this project: "%s"?' => 'Weet u zeker dat u dit project wil activeren : « %s » ?',
+ 'Project activation' => 'Project activatie',
+ 'Move the task to another project' => 'Taak verplaatsen naar een ander project',
+ 'Move to another project' => 'Verplaats naar een ander project',
+ 'Do you really want to duplicate this task?' => 'Weet u zeker dat u deze taak wil dupliceren ?',
+ 'Duplicate a task' => 'Taak dupliceren',
+ 'External accounts' => 'Externe accounts',
+ 'Account type' => 'Account type',
+ 'Local' => 'Lokaal',
+ 'Remote' => 'Remote',
+ 'Enabled' => 'Actief',
+ 'Disabled' => 'Inactief',
+ 'Google account linked' => 'Gelinkt Google Account',
+ 'Github account linked' => 'Gelinkt Github Account',
+ 'Username:' => 'Gebruikersnaam :',
+ 'Name:' => 'Naam :',
+ 'Email:' => 'Email :',
+ 'Default project:' => 'Standaard project :',
+ '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.',
+ 'Change category for the task "%s"' => 'Pas categorie aan voor taak « %s »',
+ 'Change category' => 'Categorie aanpassen',
+ '%s updated the task %s' => '%s heeft taak %s aangepast',
+ '%s opened the task %s' => '%s heeft taak %s geopend',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s heeft taak %s naar positie %d in de kolom « %s » verplaatst',
+ '%s moved the task %s to the column "%s"' => '%s heeft taak %s verplaatst naar kolom « %s »',
+ '%s created the task %s' => '%s heeft taak %s aangemaakt',
+ '%s closed the task %s' => '%s heeft taak %s gesloten',
+ '%s created a subtask for the task %s' => '%s heeft een subtaak aangemaakt voor taak %s',
+ '%s updated a subtask for the task %s' => '%s heeft een subtaak aangepast voor taak %s',
+ 'Assigned to %s with an estimate of %s/%sh' => 'Toegewezen aan %s met een schatting van %s/%sh',
+ 'Not assigned, estimate of %sh' => 'Niet toegewezen, schatting: %sh',
+ '%s updated a comment on the task %s' => '%s heeft een commentaar aangepast voor taak %s',
+ '%s commented the task %s' => '%s heeft een commentaar geplaatst voor taak %s',
+ '%s\'s activity' => 'Activiteiten van %s',
+ 'No activity.' => 'Geen activiteiten.',
+ 'RSS feed' => 'RSS feed',
+ '%s updated a comment on the task #%d' => '%s heeft een commentaar aangepast voor taak %d',
+ '%s commented on the task #%d' => '%s heeft commentaar geplaatst voor taak %d',
+ '%s updated a subtask for the task #%d' => '%s heeft een commentaar aangepast voor subtaak %d',
+ '%s created a subtask for the task #%d' => '%s heeft een subtaak aangemaakt voor taak %d',
+ '%s updated the task #%d' => '%s heeft taak %d aangepast',
+ '%s created the task #%d' => '%s heeft taak %d aangemaakt',
+ '%s closed the task #%d' => '%s heeft taak %d gesloten',
+ '%s open the task #%d' => '%s a heeft taak %d geopend',
+ '%s moved the task #%d to the column "%s"' => '%s heeft taak %d verplaatst naar kolom « %s »',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s heeft taak %d verplaatst naar positie %d in kolom « %s »',
+ 'Activity' => 'Activiteit',
+ 'Default values are "%s"' => 'Standaardwaarden zijn « %s »',
+ 'Default columns for new projects (Comma-separated)' => 'Standaard kolommen voor nieuw projecten (komma gescheiden)',
+ 'Task assignee change' => 'Taak toegewezene verandering',
+ '%s change the assignee of the task #%d to %s' => '%s heeft de toegewezene voor taak %d veranderd in %s',
+ '%s changed the assignee of the task %s to %s' => '%s heeft de toegewezene voor taak %d veranderd in %s',
+ 'Column Change' => 'Kolom verandering',
+ 'Position Change' => 'Positie verandering',
+ 'Assignee Change' => 'Toegewezene verandering',
+ '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',
+ 'Database driver:' => 'Database driver :',
+ 'Board settings' => 'Bord instellingen',
+ 'URL and token' => 'URL en token',
+ 'Webhook settings' => 'Webhook instellingen',
+ 'URL for task creation:' => 'URL voor aanmaken taken :',
+ 'Reset token' => 'Token resetten',
+ 'API endpoint:' => 'API endpoint :',
+ 'Refresh interval for private board' => 'Verversingsinterval voor private borden',
+ 'Refresh interval for public board' => 'Verversingsinterval voor publieke borden',
+ 'Task highlight period' => 'Taak highlight periode',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periode (in seconden) om aan te geven of een taak recent is aangepast (0 om uit te schakelen, standaard 2 dagen)',
+ '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 »',
+ 'New private project' => 'Nieuw privé project',
+ 'This project is private' => 'Dit project is privé',
+ 'Type here to create a new sub-task' => 'Typ hier om een nieuwe subtaak aan te maken',
+ 'Add' => 'Toevoegen',
+ '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.',
+ 'My tasks' => 'Mijn taken',
+ 'Activity stream' => 'Activiteiten',
+ 'Dashboard' => 'Dashboard',
+ // 'Confirmation' => '',
+ 'Allow everybody to access to this project' => 'Geef iedereen toegang tot dit project',
+ 'Everybody have access to this project.' => 'Iedereen heeft toegang tot dit project.',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Integration' => 'Integratue',
+ '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',
+ 'Configure' => 'Configureren',
+ 'Project management' => 'Project management',
+ 'My projects' => 'Mijn projecten',
+ 'Columns' => 'Kolommen',
+ 'Task' => 'Taak',
+ 'Your are not member of any project.' => 'U bent van geen enkel project lid.',
+ 'Percentage' => 'Percentage',
+ 'Number of tasks' => 'Aantal taken',
+ 'Task distribution' => 'Distributie van taken',
+ 'Reportings' => 'Rapporten',
+ 'Task repartition for "%s"' => 'Taakverdeling voor « %s »',
+ 'Analytics' => 'Analytics',
+ 'Subtask' => 'Subtaak',
+ 'My subtasks' => 'Mijn subtaken',
+ 'User repartition' => 'Gebruikerverdeling',
+ 'User repartition for "%s"' => 'Gebruikerverdeling voor « %s »',
+ 'Clone this project' => 'Kloon dit project',
+ 'Column removed successfully.' => 'Kolom succesvol verwijderd.',
+ 'Edit Project' => 'Project aanpassen',
+ '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',
+ 'The project id must be an integer' => 'Het project id moet een integer zijn',
+ 'The status must be an integer' => 'De status moet een integer zijn',
+ 'The subtask id is required' => 'Het id van de subtaak is verplicht',
+ 'The subtask id must be an integer' => 'Het id van de subtaak moet een integer zijn',
+ 'The task id is required' => 'Het id van de taak is verplicht',
+ 'The task id must be an integer' => 'Het id van de taak moet een integer zijn',
+ 'The user id must be an integer' => 'Het id van de gebruiker moet een integer zijn',
+ 'This value is required' => 'Deze waarde is verplicht',
+ 'This value must be numeric' => 'Deze waarde moet numeriek zijn',
+ 'Unable to create this task.' => 'Aanmaken van de taak mislukt',
+ 'Cumulative flow diagram' => 'Cummulatief stroomdiagram',
+ 'Cumulative flow diagram for "%s"' => 'Cummulatief stroomdiagram voor « %s »',
+ 'Daily project summary' => 'Dagelijkse project samenvatting',
+ 'Daily project summary export' => 'Dagelijkse project samenvatting export',
+ 'Daily project summary export for "%s"' => 'Dagelijkse project samenvatting voor « %s »',
+ 'Exports' => 'Exports',
+ 'This export contains the number of tasks per column grouped per day.' => 'Dit rapport bevat het aantal taken per kolom gegroupeerd per dag.',
+ 'Nothing to preview...' => 'Niets om te previewen...',
+ 'Preview' => 'Preview',
+ 'Write' => 'Schrijf',
+ 'Active swimlanes' => 'Actieve swinlanes',
+ 'Add a new swimlane' => 'Nieuwe swimlane toevoegen',
+ 'Change default swimlane' => 'Standaard swimlane aapassen',
+ '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.',
+ 'Swimlane removed successfully.' => 'Swimlane succesvol verwijderd.',
+ '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',
+ 'Subtasks exportation for "%s"' => 'Subtaken exporteren voor project « %s »',
+ 'Task Title' => 'Taak title',
+ 'Untitled' => 'Geen titel',
+ 'Application default' => 'Standaard taal voor applicatie',
+ 'Language:' => 'Taal :',
+ 'Timezone:' => 'Tijdzone :',
+ 'All columns' => 'Alle kolommen',
+ 'Calendar for "%s"' => 'Agenda voor « %s »',
+ 'Filter by column' => 'Filter op kolom',
+ 'Filter by status' => 'Filter op status',
+ 'Calendar' => 'Agenda',
+ 'Next' => 'Volgende',
+ '#%d' => '%d',
+ 'Filter by color' => 'Filter op kleur',
+ 'Filter by swimlane' => 'Filter op swimlane',
+ 'All swimlanes' => 'Alle swimlanes',
+ 'All colors' => 'Alle kleuren',
+ 'All status' => 'Alle statussen',
+ 'Add a comment logging moving the task between columns' => 'Voeg een commentaar toe bij het verplaatsen van een taak tussen kolommen',
+ 'Moved to column %s' => 'Verplaatst naar kolom',
+ 'Change description' => 'Verandering omschrijving',
+ 'User dashboard' => 'Gebruiker dashboard',
+ 'Allow only one subtask in progress at the same time for a user' => 'Sta maximaal één subtaak in behandeling toe per gebruiker',
+ 'Edit column "%s"' => 'Kolom « %s » aanpassen',
+ 'Enable time tracking for subtasks' => 'Activeer tijdschrijven voor subtaken',
+ 'Select the new status of the subtask: "%s"' => 'Selecteer nieuwe status voor subtaak : « %s »',
+ 'Subtask timesheet' => 'Subtaak timesheet',
+ 'There is nothing to show.' => 'Er is niets om te laten zijn.',
+ 'Time Tracking' => 'Tijdschrijven',
+ '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?',
+ 'Change dashboard view' => 'Pas dashboard aan',
+ 'Show/hide activities' => 'Toon/verberg activiteiten',
+ 'Show/hide projects' => 'Toon/verberg projecten',
+ 'Show/hide subtasks' => 'Toon/verberg subtaken',
+ 'Show/hide tasks' => 'Toon/verberg taken',
+ 'Disable login form' => 'Schakel login scherm uit',
+ 'Show/hide calendar' => 'Toon/verberg agenda',
+ 'User calendar' => 'Agenda gebruiker',
+ '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',
+ 'Days in this column' => 'Dagen in deze kolom',
+ '%dd' => '%dj',
+ 'Add a link' => 'Link toevoegen',
+ 'Add a new link' => 'Nieuwe link toevoegen',
+ 'Do you really want to remove this link: "%s"?' => 'Weet u zeker dat u deze link wil verwijderen : « %s » ?',
+ 'Do you really want to remove this link with task #%d?' => 'Weet u zeker dat u deze link met taak %d wil verwijderen?',
+ 'Field required' => 'Veld verplicht',
+ 'Link added successfully.' => 'Link succesvol toegevoegd.',
+ 'Link updated successfully.' => 'Link succesvol aangepast.',
+ 'Link removed successfully.' => 'Link succesvol verwijderd.',
+ 'Link labels' => 'Link labels',
+ 'Link modification' => 'Link aanpassing',
+ 'Links' => 'Links',
+ 'Link settings' => 'Link instellingen',
+ 'Opposite label' => 'Tegenovergesteld label',
+ 'Remove a link' => 'Link verwijderen',
+ 'Task\'s links' => 'Links van taak',
+ 'The labels must be different' => 'De labels moeten verschillend zijn',
+ 'There is no link.' => 'Er is geen link.',
+ 'This label must be unique' => 'Dit label moet uniek zijn',
+ 'Unable to create your link.' => 'Link aanmaken niet gelukt.',
+ 'Unable to update your link.' => 'Link aanpassen niet gelukt.',
+ 'Unable to remove this link.' => 'Link verwijderen niet gelukt.',
+ 'relates to' => 'is gerelateerd aan',
+ 'blocks' => 'blokkeert',
+ 'is blocked by' => 'is geblokkeerd door',
+ 'duplicates' => 'dupliceert',
+ 'is duplicated by' => 'is gedupliceerd',
+ 'is a child of' => 'is een kind van',
+ 'is a parent of' => 'is een ouder van',
+ 'targets milestone' => 'is nodig voor milestone',
+ 'is a milestone of' => 'is een milestone voor',
+ 'fixes' => 'corrigeert',
+ 'is fixed by' => 'word gecorrigeerd door',
+ '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',
+ 'Close dialog box' => 'Venster sluiten',
+ 'Submit a form' => 'Formulier insturen',
+ 'Board view' => 'Bord weergave',
+ 'Keyboard shortcuts' => 'Keyboard snelkoppelingen',
+ 'Open board switcher' => 'Open bord switcher',
+ 'Application' => 'Applicatie',
+ 'Filter recently updated' => 'Filter recent aangepast',
+ 'since %B %e, %Y at %k:%M %p' => 'sinds %d/%m/%Y à %H:%M',
+ 'More filters' => 'Meer filters',
+ // 'Compact view' => '',
+ // 'Horizontal scrolling' => '',
+ // 'Compact/wide view' => '',
+ // 'No results match:' => '',
+ // 'Remove hourly rate' => '',
+ // 'Do you really want to remove this hourly rate?' => '',
+ // 'Hourly rates' => '',
+ // 'Hourly rate' => '',
+ // 'Currency' => '',
+ // 'Effective date' => '',
+ // 'Add new rate' => '',
+ // 'Rate removed successfully.' => '',
+ // 'Unable to remove this rate.' => '',
+ // 'Unable to save the hourly rate.' => '',
+ // 'Hourly rate created successfully.' => '',
+ // 'Start time' => '',
+ // 'End time' => '',
+ // 'Comment' => '',
+ // 'All day' => '',
+ // 'Day' => '',
+ // 'Manage timetable' => '',
+ // 'Overtime timetable' => '',
+ // 'Time off timetable' => '',
+ // 'Timetable' => '',
+ // 'Work timetable' => '',
+ // 'Week timetable' => '',
+ // 'Day timetable' => '',
+ // 'From' => '',
+ // 'To' => '',
+ // 'Time slot created successfully.' => '',
+ // 'Unable to save this time slot.' => '',
+ // 'Time slot removed successfully.' => '',
+ // 'Unable to remove this time slot.' => '',
+ // 'Do you really want to remove this time slot?' => '',
+ // 'Remove time slot' => '',
+ // 'Add new time slot' => '',
+ // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
+ // 'Files' => '',
+ // 'Images' => '',
+ // 'Private project' => '',
+ // 'Amount' => '',
+ // 'AUD - Australian Dollar' => '',
+ // 'Budget' => '',
+ // 'Budget line' => '',
+ // 'Budget line removed successfully.' => '',
+ // 'Budget lines' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ // 'Cost' => '',
+ // 'Cost breakdown' => '',
+ // 'Custom Stylesheet' => '',
+ // 'download' => '',
+ // 'Do you really want to remove this budget line?' => '',
+ // 'EUR - Euro' => '',
+ // 'Expenses' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'New budget line' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'Remove a budget line' => '',
+ // 'Remove budget line' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'The budget line have been created successfully.' => '',
+ // 'Unable to create the budget line.' => '',
+ // 'Unable to remove this budget line.' => '',
+ // 'USD - US Dollar' => '',
+ // 'Remaining' => '',
+ // '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' => '',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ // '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' => '',
+ // 'Currency rates are used to calculate project budget.' => '',
+ // 'Reference currency' => '',
+ // 'The currency rate have been added successfully.' => '',
+ // 'Unable to add this currency rate.' => '',
+ // 'Send notifications to a Slack channel' => '',
+ // 'Webhook URL' => '',
+ // 'Help on Slack integration' => '',
+ // '%s remove the assignee of the task %s' => '',
+ // 'Send notifications to Hipchat' => '',
+ // 'API URL' => '',
+ // 'Room API ID or name' => '',
+ // 'Room notification token' => '',
+ // 'Help on Hipchat integration' => '',
+ // '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.' => '',
+ // 'SEK - Swedish Krona' => '',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
+);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
new file mode 100644
index 00000000..9e3fe96c
--- /dev/null
+++ b/app/Locale/pl_PL/translations.php
@@ -0,0 +1,925 @@
+<?php
+
+return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
+ 'None' => 'Brak',
+ 'edit' => 'edytuj',
+ 'Edit' => 'Edytuj',
+ 'remove' => 'usuń',
+ 'Remove' => 'Usuń',
+ 'Update' => 'Aktualizuj',
+ 'Yes' => 'Tak',
+ 'No' => 'Nie',
+ 'cancel' => 'anuluj',
+ 'or' => 'lub',
+ 'Yellow' => 'Żółty',
+ 'Blue' => 'Niebieski',
+ 'Green' => 'Zielony',
+ 'Purple' => 'Fioletowy',
+ 'Red' => 'Czerwony',
+ 'Orange' => 'Pomarańczowy',
+ 'Grey' => 'Szary',
+ 'Save' => 'Zapisz',
+ 'Login' => 'Login',
+ 'Official website:' => 'Oficjalna strona:',
+ 'Unassigned' => 'Nieprzypisany',
+ 'View this task' => 'Zobacz zadanie',
+ 'Remove user' => 'Usuń użytkownika',
+ 'Do you really want to remove this user: "%s"?' => 'Na pewno chcesz usunąć użytkownika: "%s"?',
+ 'New user' => 'Nowy użytkownik',
+ 'All users' => 'Wszyscy użytkownicy',
+ 'Username' => 'Nazwa użytkownika',
+ 'Password' => 'Hasło',
+ 'Default project' => 'Domyślny projekt',
+ 'Administrator' => 'Administrator',
+ 'Sign in' => 'Zaloguj',
+ 'Users' => 'Użytkownicy',
+ 'No user' => 'Brak użytkowników',
+ 'Forbidden' => 'Zabroniony',
+ 'Access Forbidden' => 'Dostęp zabroniony',
+ 'Only administrators can access to this page.' => 'Tylko administrator może wejść na tą stronę.',
+ 'Edit user' => 'Edytuj użytkownika',
+ 'Logout' => 'Wyloguj',
+ 'Bad username or password' => 'Zła nazwa uyżytkownika lub hasło',
+ 'users' => 'użytkownicy',
+ 'projects' => 'projekty',
+ 'Edit project' => 'Edytuj projekt',
+ 'Name' => 'Nazwa',
+ 'Activated' => 'Aktywny',
+ 'Projects' => 'Projekty',
+ 'No project' => 'Brak projektów',
+ 'Project' => 'Projekt',
+ 'Status' => 'Status',
+ 'Tasks' => 'Zadania',
+ 'Board' => 'Tablica',
+ 'Actions' => 'Akcje',
+ 'Inactive' => 'Nieaktywny',
+ 'Active' => 'Aktywny',
+ 'Column %d' => 'Kolumna %d',
+ 'Add this column' => 'Dodaj kolumnę',
+ '%d tasks on the board' => '%d zadań na tablicy',
+ '%d tasks in total' => '%d wszystkich zadań',
+ 'Unable to update this board.' => 'Nie można zaktualizować tablicy.',
+ 'Edit board' => 'Edytuj tablicę',
+ 'Disable' => 'Wyłącz',
+ 'Enable' => 'Włącz',
+ 'New project' => 'Nowy projekt',
+ 'Do you really want to remove this project: "%s"?' => 'Na pewno chcesz usunąć projekt: "%s"?',
+ 'Remove project' => 'Usuń projekt',
+ 'Boards' => 'Tablice',
+ 'Edit the board for "%s"' => 'Edytuj tablię dla "%s"',
+ 'All projects' => 'Wszystkie projekty',
+ 'Change columns' => 'Zmień kolumny',
+ 'Add a new column' => 'Dodaj nową kolumnę',
+ 'Title' => 'Tytuł',
+ 'Add Column' => 'Dodaj kolumnę',
+ 'Project "%s"' => 'Projekt "%s"',
+ 'Nobody assigned' => 'Nikt nie przypisany',
+ 'Assigned to %s' => 'Przypisane do %s',
+ 'Remove a column' => 'Usuń kolumnę',
+ 'Remove a column from a board' => 'Usuń kolumnę z tablicy',
+ 'Unable to remove this column.' => 'Nie udało się usunąć kolumny.',
+ 'Do you really want to remove this column: "%s"?' => 'Na pewno chcesz usunąć kolumnę: "%s"?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Wszystkie zadania w kolumnie zostaną usunięte!',
+ 'Settings' => 'Ustawienia',
+ 'Application settings' => 'Ustawienia aplikacji',
+ 'Language' => 'Język',
+ 'Webhook token:' => 'Token :',
+ 'API token:' => 'Token dla API',
+ 'More information' => 'Więcej informacji',
+ 'Database size:' => 'Rozmiar bazy danych :',
+ 'Download the database' => 'Pobierz bazę danych',
+ 'Optimize the database' => 'Optymalizuj bazę danych',
+ '(VACUUM command)' => '(komenda VACUUM)',
+ '(Gzip compressed Sqlite file)' => '(baza danych spakowana Gzip)',
+ 'User settings' => 'Ustawienia użytkownika',
+ 'My default project:' => 'Mój domyślny projekt:',
+ 'Close a task' => 'Zakończ zadanie',
+ 'Do you really want to close this task: "%s"?' => 'Na pewno chcesz zakończyć to zadanie: "%s"?',
+ 'Edit a task' => 'Edytuj zadanie',
+ 'Column' => 'Kolumna',
+ 'Color' => 'Kolor',
+ 'Assignee' => 'Odpowiedzialny',
+ 'Create another task' => 'Dodaj kolejne zadanie',
+ 'New task' => 'Nowe zadanie',
+ '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.',
+ 'Add a new task' => 'Dodaj zadanie',
+ 'The username is required' => 'Nazwa użytkownika jest wymagana',
+ 'The maximum length is %d characters' => 'Maksymalna długość wynosi %d znaków',
+ 'The minimum length is %d characters' => 'Minimalna długość wynosi %d znaków',
+ 'The password is required' => 'Hasło jest wymagane',
+ 'This value must be an integer' => 'Wartość musi być liczbą całkowitą',
+ 'The username must be unique' => 'Nazwa użytkownika musi być unikalna',
+ 'The username must be alphanumeric' => 'Nazwa użytkownika musi być alfanumeryczna',
+ 'The user id is required' => 'ID użytkownika jest wymagane',
+ 'Passwords don\'t match' => 'Hasła nie pasują do siebie',
+ 'The confirmation is required' => 'Wymagane jest potwierdzenie',
+ 'The column is required' => 'Kolumna jest wymagana',
+ 'The project is required' => 'Projekt jest wymagany',
+ 'The color is required' => 'Kolor jest wymagany',
+ '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',
+ 'The language is required' => 'Język jest wymagany',
+ 'There is no active project, the first step is to create a new project.' => 'Brak aktywnych projektów. Pierwszym krokiem jest utworzenie nowego projektu.',
+ 'Settings saved successfully.' => 'Ustawienia zapisane.',
+ 'Unable to save your settings.' => 'Nie udało się zapisać ustawień.',
+ 'Database optimization done.' => 'Optymalizacja bazy danych zakończona.',
+ 'Your project have been created successfully.' => 'Projekt został pomyślnie utworzony.',
+ 'Unable to create your project.' => 'Nie udało się stworzyć projektu.',
+ 'Project updated successfully.' => 'Projekt zaktualizowany.',
+ 'Unable to update this project.' => 'Nie można zaktualizować projektu.',
+ 'Unable to remove this project.' => 'Nie można usunąć projektu.',
+ 'Project removed successfully.' => 'Projekt usunięty.',
+ 'Project activated successfully.' => 'Projekt aktywowany.',
+ 'Unable to activate this project.' => 'Nie można aktywować projektu.',
+ 'Project disabled successfully.' => 'Projekt wyłączony.',
+ 'Unable to disable this project.' => 'Nie można wyłączyć projektu.',
+ 'Unable to open this task.' => 'Nie można otworzyć tego zadania.',
+ 'Task opened successfully.' => 'Zadanie otwarte.',
+ 'Unable to close this task.' => 'Nie można zamknąć tego zadania.',
+ 'Task closed successfully.' => 'Zadanie zamknięte.',
+ 'Unable to update your task.' => 'Nie można zaktualizować tego zadania.',
+ 'Task updated successfully.' => 'Zadanie zaktualizowane.',
+ 'Unable to create your task.' => 'Nie można dodać zadania.',
+ '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.',
+ '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.',
+ 'Board updated successfully.' => 'Tablica została zaktualizowana.',
+ 'Ready' => 'Gotowe',
+ 'Backlog' => 'Log',
+ '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',
+ 'No task' => 'Brak zadań',
+ 'Completed tasks' => 'Ukończone zadania',
+ 'List of projects' => 'Lista projektów',
+ 'Completed tasks for "%s"' => 'Zadania zakończone dla "%s"',
+ '%d closed tasks' => '%d zamkniętych zadań',
+ 'No task for this project' => 'Brak zadań dla tego projektu',
+ 'Public link' => 'Link publiczny',
+ 'There is no column in your project!' => 'Brak kolumny w Twoim projekcie',
+ 'Change assignee' => 'Zmień odpowiedzialną osobę',
+ 'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"',
+ 'Timezone' => 'Strefa czasowa',
+ 'Sorry, I didn\'t find this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych',
+ 'Page not found' => 'Strona nie istnieje',
+ 'Complexity' => 'Poziom trudności',
+ 'limit' => 'limit',
+ 'Task limit' => 'Limit zadań',
+ 'Task count' => 'Liczba zadań',
+ 'This value must be greater than %d' => 'Wartość musi być większa niż %d',
+ 'Edit project access list' => 'Edycja list dostępu dla projektu',
+ 'Edit users access' => 'Edytuj dostęp',
+ 'Allow this user' => 'Dodaj użytkownika',
+ 'Only those users have access to this project:' => 'Użytkownicy mający dostęp:',
+ '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',
+ 'You are not allowed to access to this project.' => 'Nie masz dostępu do tego projektu.',
+ 'Comments' => 'Komentarze',
+ 'Post comment' => 'Dodaj komentarz',
+ 'Write your text in Markdown' => 'Możesz użyć Markdown',
+ 'Leave a comment' => 'Zostaw komentarz',
+ 'Comment is required' => 'Komentarz jest wymagany',
+ 'Leave a description' => 'Dodaj opis',
+ 'Comment added successfully.' => 'Komentarz dodany',
+ 'Unable to create your comment.' => 'Nie udało się dodać komentarza',
+ 'The description is required' => 'Opis jest wymagany',
+ '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' => '',
+ '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',
+ 'Remove an action' => 'Usuń akcję',
+ 'Unable to remove this action.' => 'Nie można usunąć akcji',
+ 'Action removed successfully.' => 'Akcja usunięta',
+ 'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"',
+ 'Defined actions' => 'Zdefiniowane akcje',
+ 'Add an action' => 'Nowa akcja',
+ 'Event name' => 'Nazwa zdarzenia',
+ 'Action name' => 'Nazwa akcji',
+ 'Action parameters' => 'Parametry akcji',
+ 'Action' => 'Akcja',
+ 'Event' => 'Zdarzenie',
+ 'When the selected event occurs execute the corresponding action.' => 'Gdy następuje wybrane zdarzenie, uruchom odpowiednią akcję',
+ 'Next step' => 'Następny krok',
+ 'Define action parameters' => 'Zdefiniuj parametry akcji',
+ 'Save this action' => 'Zapisz akcję',
+ 'Do you really want to remove this action: "%s"?' => 'Na pewno chcesz usunąć akcję "%s"?',
+ 'Remove an automatic action' => 'Usuń akcję automatyczną',
+ 'Close the task' => 'Zamknij zadanie',
+ 'Assign the task to a specific user' => 'Przypisz zadanie do wybranego użytkownika',
+ 'Assign the task to the person who does the action' => 'Przypisz zadanie to osoby wykonującej akcję',
+ 'Duplicate the task to another project' => 'Kopiuj zadanie do innego projektu',
+ 'Move a task to another column' => 'Przeniesienie zadania do innej kolumny',
+ 'Move a task to another position in the same column' => 'Zmiania pozycji zadania w kolumnie',
+ 'Task modification' => 'Modyfikacja zadania',
+ 'Task creation' => 'Tworzenie zadania',
+ 'Open a closed task' => 'Otwarcie zamkniętego zadania',
+ 'Closing a task' => 'Zamknięcie zadania',
+ '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',
+ 'Update this comment' => 'Zapisz komentarz',
+ 'Comment updated successfully.' => 'Komentarz został zapisany.',
+ 'Unable to update your comment.' => 'Nie udało się zapisanie 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.',
+ 'Details' => 'Szczegóły',
+ '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',
+ 'Reset all tokens' => 'Zresetuj wszystkie tokeny',
+ 'All tokens have been regenerated.' => 'Wszystkie tokeny zostały zresetowane.',
+ 'Unknown' => 'Nieznany',
+ 'Last logins' => 'Ostatnie logowania',
+ 'Login date' => 'Data logowania',
+ 'Authentication method' => 'Sposób autentykacji',
+ 'IP address' => 'Adres IP',
+ 'User agent' => 'Przeglądarka',
+ 'Persistent connections' => 'Stałe połączenia',
+ 'No session.' => 'Brak sesji.',
+ 'Expiration date' => 'Data zakończenia',
+ 'Remember Me' => 'Pamiętaj mnie',
+ 'Creation date' => 'Data utworzenia',
+ 'Filter by user' => 'Filtruj według użytkowników',
+ 'Filter by due date' => 'Filtruj według terminów',
+ 'Everybody' => 'Wszyscy',
+ 'Open' => 'Otwarto',
+ 'Closed' => 'Zamknięto',
+ 'Search' => 'Szukaj',
+ 'Nothing found.' => 'Nic nie znaleziono',
+ 'Search in the project "%s"' => 'Szukaj w projekcie "%s"',
+ 'Due date' => 'Termin',
+ 'Others formats accepted: %s and %s' => 'Inne akceptowane formaty: %s and %s',
+ 'Description' => 'Opis',
+ '%d comments' => '%d Komentarzy',
+ '%d comment' => '%d Komentarz',
+ 'Email address invalid' => 'Błędny adres email',
+ 'Your Google Account is not linked anymore to your profile.' => 'Twoje konto Google nie jest już połączone',
+ 'Unable to unlink your Google Account.' => 'Nie można odłączyć konta Google',
+ 'Google authentication failed' => 'Autentykacja Google nieudana',
+ 'Unable to link your Google Account.' => 'Nie można podłączyć konta Google',
+ 'Your Google Account is linked to your profile successfully.' => 'Podłączanie konta Google ukończone pomyślnie',
+ '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 #%d' => 'Zadanie #%d',
+ 'Task removed successfully.' => 'Zadanie usunięto pomyślnie.',
+ 'Unable to remove this task.' => 'Nie można usunąć tego zadania.',
+ 'Remove a task' => 'Usuń zadanie',
+ 'Do you really want to remove this task: "%s"?' => 'Czy na pewno chcesz usunąć zadanie "%s"?',
+ 'Assign automatically a color based on a category' => 'Przypisz kolor automatycznie na podstawie kategori',
+ 'Assign automatically a category based on a color' => 'Przypisz kategorię automatycznie na podstawie koloru',
+ 'Task creation or modification' => 'Tworzenie lub usuwanie zadania',
+ 'Category' => 'Kategoria',
+ 'Category:' => 'Kategoria:',
+ 'Categories' => 'Kategorie',
+ 'Category not found.' => 'Kategoria nie istnieje',
+ 'Your category have been created successfully.' => 'Pomyślnie utworzono kategorię.',
+ 'Unable to create your category.' => 'Nie można tworzyć kategorii.',
+ 'Your category have been updated successfully.' => 'Pomyślnie zaktualizowano kategorię',
+ 'Unable to update your category.' => 'Nie można zaktualizować kategorii',
+ 'Remove a category' => 'Usuń kategorię',
+ 'Category removed successfully.' => 'Pomyślnie usunięto kategorię.',
+ 'Unable to remove this category.' => 'Nie można usunąć tej kategorii.',
+ 'Category modification for the project "%s"' => 'Zmiania kategorii projektu "%s"',
+ 'Category Name' => 'Nazwa kategorii',
+ 'Categories for the project "%s"' => 'Kategorie projektu',
+ 'Add a new category' => 'Utwórz nową kategorię',
+ 'Do you really want to remove this category: "%s"?' => 'Czy na pewno chcesz usunąć kategorię: "%s"?',
+ 'Filter by category' => 'Filtruj według kategorii',
+ 'All categories' => 'Wszystkie kategorie',
+ 'No category' => 'Brak kategorii',
+ 'The name is required' => 'Nazwa jest wymagana',
+ 'Remove a file' => 'Usuń plik',
+ 'Unable to remove this file.' => 'Nie można usunąć tego pliku.',
+ 'File removed successfully.' => 'Plik Usunięty pomyślnie.',
+ 'Attach a document' => 'Dołącz plik',
+ 'Do you really want to remove this file: "%s"?' => 'Czy na pewno chcesz usunąć plik: "%s"?',
+ 'open' => 'otwórz',
+ 'Attachments' => 'Załączniki',
+ 'Edit the task' => 'Edytuj Zadanie',
+ 'Edit the description' => 'Edytuj opis',
+ 'Add a comment' => 'Dodaj komentarz',
+ 'Edit a comment' => 'Edytuj komentarz',
+ 'Summary' => 'Podsumowanie',
+ 'Time tracking' => 'Śledzenie czasu',
+ 'Estimate:' => 'Szacowany:',
+ 'Spent:' => 'Przeznaczony:',
+ 'Do you really want to remove this sub-task?' => 'Czy na pewno chcesz usunąć to pod-zadanie?',
+ 'Remaining:' => 'Pozostało:',
+ 'hours' => 'godzin',
+ 'spent' => 'przeznaczono',
+ 'estimated' => 'szacowany',
+ 'Sub-Tasks' => 'Pod-zadanie',
+ 'Add a sub-task' => 'Dodaj pod-zadanie',
+ 'Original estimate' => 'Szacowanie początkowe',
+ 'Create another sub-task' => 'Dodaj kolejne pod-zadanie',
+ 'Time spent' => 'Przeznaczony czas',
+ 'Edit a sub-task' => 'Edytuj pod-zadanie',
+ 'Remove a sub-task' => 'Usuń pod-zadanie',
+ 'The time must be a numeric value' => 'Czas musi być wartością liczbową',
+ '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.',
+ 'Sub-task updated successfully.' => 'Pod-zadanie zaktualizowane pomyślnie.',
+ 'Unable to update your sub-task.' => 'Nie można zaktalizować tego pod-zadania.',
+ 'Unable to create your sub-task.' => 'Nie można utworzyć tego pod-zadania.',
+ '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',
+ 'Your GitHub account was successfully linked to your profile.' => 'Konto Github podłączone pomyślnie.',
+ 'Unable to link your GitHub Account.' => 'Nie można połączyć z kontem Github.',
+ 'GitHub authentication failed' => 'Autentykacja Github nieudana',
+ 'Your GitHub account is no longer linked to your profile.' => 'Konto Github nie jest już podłączone do twojego profilu.',
+ 'Unable to unlink your GitHub Account.' => 'Nie można odłączyć konta Github.',
+ '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',
+ 'End Date' => 'Data Końcowa',
+ 'Execute' => 'Wykonaj',
+ 'Task Id' => 'Identyfikator Zadania',
+ 'Creator' => 'Autor',
+ 'Modification date' => 'Data modyfyfikacji',
+ 'Completion date' => 'Data ukończenia',
+ 'Clone' => 'Sklonuj',
+ 'Clone Project' => 'Sklonuj projekt',
+ 'Project cloned successfully.' => 'Projekt sklonowany pomyślnie.',
+ 'Unable to clone this project.' => 'Nie można sklonować projektu.',
+ 'Email notifications' => 'Powiadomienia email',
+ 'Enable email notifications' => 'Włącz powiadomienia email',
+ 'Task position:' => 'Pozycja zadania:',
+ '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ł:',
+ // 'Status:' => '',
+ 'Assignee:' => 'Przypisano do:',
+ 'Time tracking:' => 'Śledzenie czasu: ',
+ 'New sub-task' => 'Nowe Pod-zadanie',
+ 'New attachment added "%s"' => 'Nowy załącznik dodany "%s"',
+ 'Comment updated' => 'Komentarz zaktualizowany',
+ 'New comment posted by %s' => 'Nowy komentarz dodany przez %s',
+ 'List of due tasks for the project "%s"' => 'Lista zadań oczekujących projektu "%s"',
+ // 'New attachment' => '',
+ // 'New comment' => '',
+ // 'New subtask' => '',
+ // 'Subtask updated' => '',
+ // 'Task updated' => '',
+ // 'Task closed' => '',
+ // 'Task opened' => '',
+ '[%s][Due tasks]' => '[%s][Zadania oczekujące]',
+ '[Kanboard] Notification' => '[Kanboard] Powiadomienie',
+ 'I want to receive notifications only for those projects:' => 'Chcę otrzymywaćpowiadiomienia tylko dla tych projektów:',
+ 'view the task on Kanboard' => 'Zobacz zadanie',
+ 'Public access' => 'Dostęp publiczny',
+ 'Category management' => 'Zarządzanie kategoriami',
+ 'User management' => 'Zarządzanie użytkownikami',
+ 'Active tasks' => 'Aktywne zadania',
+ 'Disable public access' => 'Zablokuj dostęp publiczny',
+ 'Enable public access' => 'Odblokuj dostęp publiczny',
+ 'Active projects' => 'Aktywne projety',
+ 'Inactive projects' => 'Nieaktywne projekty',
+ 'Public access disabled' => 'Dostęp publiczny zablokowany',
+ 'Do you really want to disable this project: "%s"?' => 'Czy napewno chcesz zablokować projekt: "%s"?',
+ 'Do you really want to duplicate this project: "%s"?' => 'Czy napewno chcesz zduplikować projekt: "%s"?',
+ 'Do you really want to enable this project: "%s"?' => 'Czy napewno chcesz odblokować projekt: "%s"?',
+ 'Project activation' => 'Aktywacja projekt',
+ 'Move the task to another project' => 'Przenieś zadanie do innego projektu',
+ 'Move to another project' => 'Przenieś do innego projektu',
+ 'Do you really want to duplicate this task?' => 'Czy napewno chcesz zduplikować to zadanie: "%s"?',
+ 'Duplicate a task' => 'Zduplikuj zadanie',
+ 'External accounts' => 'Konta zewnętrzne',
+ 'Account type' => 'Typ konta',
+ 'Local' => 'Lokalne',
+ 'Remote' => 'Zdalne',
+ 'Enabled' => 'Odblokowane',
+ 'Disabled' => 'Zablokowane',
+ 'Google account linked' => 'Połączone konto Google',
+ 'Github account linked' => 'Połączone konto Github',
+ 'Username:' => 'Nazwa Użytkownika:',
+ 'Name:' => 'Imię i Nazwisko',
+ 'Email:' => 'Email: ',
+ 'Default project:' => 'Projekt domyślny:',
+ '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' => 'Zmiania 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.',
+ 'Change category for the task "%s"' => 'Zmień kategorię dla zadania "%s"',
+ 'Change category' => 'Zmień kategorię',
+ '%s updated the task %s' => '%s zaktualizował zadanie %s',
+ '%s opened the task %s' => '%s otworzył zadanie %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s przeniósł zadanie %s na pozycję #%d w kolumnie "%s"',
+ '%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',
+ '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',
+ '%s commented the task %s' => '%s skomentował zadanie %s',
+ '%s\'s activity' => 'Aktywność %s',
+ 'No activity.' => 'Brak aktywności.',
+ '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 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',
+ '%s open the task #%d' => '%s otworzył zadanie #%d',
+ '%s moved the task #%d to the column "%s"' => '%s przeniósł zadanie #%d do kolumny "%s"',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s przeniósł zadanie #%d na pozycję %d w kolmnie "%s"',
+ 'Activity' => 'Aktywność',
+ 'Default values are "%s"' => 'Domyślne wartości: "%s"',
+ 'Default columns for new projects (Comma-separated)' => 'Domyślne kolmny dla nowych projektów (oddzielone przecinkiem)',
+ 'Task assignee change' => 'Zmień osobę odpowiedzialną',
+ '%s change the assignee of the task #%d to %s' => '%s zmienił osobę odpowiedzialną za zadanie #%d na %s',
+ '%s changed the assignee of the task %s to %s' => '%s zmienił osobę odpowiedzialną za zadanie %s na %s',
+ // 'Column Change' => '',
+ // 'Position Change' => '',
+ // 'Assignee Change' => '',
+ '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ętrzenj etykiety',
+ // 'Reference' => '',
+ // 'Reference: %s' => '',
+ 'Label' => 'Etykieta',
+ 'Database' => 'Baza danych',
+ 'About' => 'Informacje',
+ 'Database driver:' => 'Silnik bazy danych:',
+ 'Board settings' => 'Ustawienia tablicy',
+ 'URL and token' => 'URL i token',
+ // 'Webhook settings' => '',
+ 'URL for task creation:' => 'URL do tworzenia zadań',
+ 'Reset token' => 'Resetuj token',
+ // 'API endpoint:' => '',
+ 'Refresh interval for private board' => 'Częstotliwość odświerzania dla tablicy prywatnej',
+ 'Refresh interval for public board' => 'Częstotliwość odświerzania dla tablicy publicznej',
+ 'Task highlight period' => 'Okres wyróżniania zadań',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Okres (w sekundach) wymagany do uznania projektu za niedawno zmieniony (0 ab zablokować, domyślnie 2 dni)',
+ 'Frequency in second (60 seconds by default)' => 'Częstotliwosć 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',
+ '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',
+ 'Confirmation' => 'Potwierdzenie',
+ 'Allow everybody to access to this project' => 'Udostepnij ten projekt wszystkim',
+ 'Everybody have access to this project.' => 'Wszyscy mają dostęp do tego projektu.',
+ // 'Webhooks' => '',
+ // 'API' => '',
+ 'Integration' => 'Integracja',
+ // 'Github webhooks' => '',
+ // 'Help on Github webhooks' => '',
+ 'Create a comment from an external provider' => 'Utwórz komentarz od zewnętrznego dostawcy',
+ // 'Github issue comment created' => '',
+ 'Configure' => 'Konfiguruj',
+ 'Project management' => 'Menadżer projektu',
+ 'My projects' => 'Moje projekty',
+ 'Columns' => 'Kolumny',
+ 'Task' => 'zadania',
+ 'Your are not member of any project.' => 'Nie bierzesz udziału w żadnym projekcie',
+ 'Percentage' => 'Procent',
+ 'Number of tasks' => 'Liczba zadań',
+ 'Task distribution' => 'Rozmieszczenie zadań',
+ 'Reportings' => 'Raporty',
+ 'Task repartition for "%s"' => 'Przydział zadań dla "%s"',
+ 'Analytics' => 'Analizy',
+ 'Subtask' => 'Pod-zadanie',
+ 'My subtasks' => 'Moje pod-zadania',
+ '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 pomyslnie.',
+ 'Edit Project' => 'Edytuj projekt',
+ // '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 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ą',
+ 'This value is required' => 'Wymagana wartość',
+ 'This value must be numeric' => 'Wartość musi być liczbą',
+ '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 export' => 'Eksport dziennego podsumowania projektu',
+ 'Daily project summary export for "%s"' => 'Eksport dziennego podsumowania projektu dla "%s"',
+ 'Exports' => 'Eksporty',
+ 'This export contains the number of tasks per column grouped per day.' => 'Ten eksport zawiera ilość zadań zgrupowanych w kolumnach na dzień',
+ 'Nothing to preview...' => 'Nic do podejrzenia...',
+ 'Preview' => 'Podgląd',
+ 'Write' => 'Edycja',
+ 'Active swimlanes' => 'Aktywne procesy',
+ 'Add a new swimlane' => 'Dodaj proces',
+ 'Change default swimlane' => 'Zmień domyślny proces',
+ '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.',
+ 'Swimlane removed successfully.' => 'Proces usunięty pomyslnie.',
+ '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śći, 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',
+ 'Application default' => 'Domyślne dla aplikacji',
+ 'Language:' => 'Język:',
+ 'Timezone:' => 'Strefa czasowa:',
+ 'All columns' => 'Wszystkie kolumny',
+ 'Calendar for "%s"' => 'Kalendarz dla "%s"',
+ 'Filter by column' => 'Filtrj według kolumn',
+ 'Filter by status' => 'Filtruj według statusu',
+ 'Calendar' => 'Kalendarz',
+ 'Next' => 'Następny',
+ // '#%d' => '',
+ 'Filter by color' => 'Filtruj według koloru',
+ 'Filter by swimlane' => 'Filtruj według procesu',
+ 'All swimlanes' => 'Wszystkie procesy',
+ 'All colors' => 'Wszystkie kolory',
+ 'All status' => 'Wszystkie statusy',
+ 'Add a comment logging moving the task between columns' => 'Dodaj komentarz dokumentujący przeniesienie zadania pomiędzy kolumnami',
+ 'Moved to column %s' => 'Przeniosiono do kolumny %s',
+ // 'Change description' => '',
+ // 'User dashboard' => '',
+ // 'Allow only one subtask in progress at the same time for a user' => '',
+ // 'Edit column "%s"' => '',
+ // 'Enable time tracking for subtasks' => '',
+ // 'Select the new status of the subtask: "%s"' => '',
+ // 'Subtask timesheet' => '',
+ // 'There is nothing to show.' => '',
+ // 'Time Tracking' => '',
+ // 'You already have one subtask in progress' => '',
+ // 'Which parts of the project do you want to duplicate?' => '',
+ // 'Change dashboard view' => '',
+ // 'Show/hide activities' => '',
+ // 'Show/hide projects' => '',
+ // 'Show/hide subtasks' => '',
+ // 'Show/hide tasks' => '',
+ // 'Disable login form' => '',
+ // 'Show/hide calendar' => '',
+ // 'User calendar' => '',
+ // 'Bitbucket commit received' => '',
+ // 'Bitbucket webhooks' => '',
+ // 'Help on Bitbucket webhooks' => '',
+ // 'Start' => '',
+ // 'End' => '',
+ // 'Task age in days' => '',
+ // 'Days in this column' => '',
+ // '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
+ // 'Filter recently updated' => '',
+ // 'since %B %e, %Y at %k:%M %p' => '',
+ // 'More filters' => '',
+ // 'Compact view' => '',
+ // 'Horizontal scrolling' => '',
+ // 'Compact/wide view' => '',
+ // 'No results match:' => '',
+ // 'Remove hourly rate' => '',
+ // 'Do you really want to remove this hourly rate?' => '',
+ // 'Hourly rates' => '',
+ // 'Hourly rate' => '',
+ // 'Currency' => '',
+ // 'Effective date' => '',
+ // 'Add new rate' => '',
+ // 'Rate removed successfully.' => '',
+ // 'Unable to remove this rate.' => '',
+ // 'Unable to save the hourly rate.' => '',
+ // 'Hourly rate created successfully.' => '',
+ // 'Start time' => '',
+ // 'End time' => '',
+ // 'Comment' => '',
+ // 'All day' => '',
+ // 'Day' => '',
+ // 'Manage timetable' => '',
+ // 'Overtime timetable' => '',
+ // 'Time off timetable' => '',
+ // 'Timetable' => '',
+ // 'Work timetable' => '',
+ // 'Week timetable' => '',
+ // 'Day timetable' => '',
+ // 'From' => '',
+ // 'To' => '',
+ // 'Time slot created successfully.' => '',
+ // 'Unable to save this time slot.' => '',
+ // 'Time slot removed successfully.' => '',
+ // 'Unable to remove this time slot.' => '',
+ // 'Do you really want to remove this time slot?' => '',
+ // 'Remove time slot' => '',
+ // 'Add new time slot' => '',
+ // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
+ // 'Files' => '',
+ // 'Images' => '',
+ // 'Private project' => '',
+ // 'Amount' => '',
+ // 'AUD - Australian Dollar' => '',
+ // 'Budget' => '',
+ // 'Budget line' => '',
+ // 'Budget line removed successfully.' => '',
+ // 'Budget lines' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ // 'Cost' => '',
+ // 'Cost breakdown' => '',
+ // 'Custom Stylesheet' => '',
+ // 'download' => '',
+ // 'Do you really want to remove this budget line?' => '',
+ // 'EUR - Euro' => '',
+ // 'Expenses' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'New budget line' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'Remove a budget line' => '',
+ // 'Remove budget line' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'The budget line have been created successfully.' => '',
+ // 'Unable to create the budget line.' => '',
+ // 'Unable to remove this budget line.' => '',
+ // 'USD - US Dollar' => '',
+ // 'Remaining' => '',
+ // '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' => '',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ // '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' => '',
+ // 'Currency rates are used to calculate project budget.' => '',
+ // 'Reference currency' => '',
+ // 'The currency rate have been added successfully.' => '',
+ // 'Unable to add this currency rate.' => '',
+ // 'Send notifications to a Slack channel' => '',
+ // 'Webhook URL' => '',
+ // 'Help on Slack integration' => '',
+ // '%s remove the assignee of the task %s' => '',
+ // 'Send notifications to Hipchat' => '',
+ // 'API URL' => '',
+ // 'Room API ID or name' => '',
+ // 'Room notification token' => '',
+ // 'Help on Hipchat integration' => '',
+ // '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.' => '',
+ // 'SEK - Swedish Krona' => '',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
+);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
new file mode 100644
index 00000000..d16916df
--- /dev/null
+++ b/app/Locale/pt_BR/translations.php
@@ -0,0 +1,925 @@
+<?php
+
+return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => ' ',
+ 'None' => 'Nenhum',
+ 'edit' => 'editar',
+ 'Edit' => 'Editar',
+ 'remove' => 'remover',
+ 'Remove' => 'Remover',
+ 'Update' => 'Atualizar',
+ 'Yes' => 'Sim',
+ 'No' => 'Não',
+ 'cancel' => 'cancelar',
+ 'or' => 'ou',
+ 'Yellow' => 'Amarelo',
+ 'Blue' => 'Azul',
+ 'Green' => 'Verde',
+ 'Purple' => 'Roxo',
+ 'Red' => 'Vermelho',
+ 'Orange' => 'Laranja',
+ 'Grey' => 'Cinza',
+ 'Save' => 'Salvar',
+ 'Login' => 'Login',
+ 'Official website:' => 'Site oficial:',
+ 'Unassigned' => 'Não Atribuída',
+ 'View this task' => 'Ver esta tarefa',
+ 'Remove user' => 'Remover usuário',
+ 'Do you really want to remove this user: "%s"?' => 'Você realmente deseja remover este usuário: "%s"?',
+ 'New user' => 'Novo usuário',
+ 'All users' => 'Todos os usuários',
+ 'Username' => 'Nome de usuário',
+ 'Password' => 'Senha',
+ 'Default project' => 'Projeto padrão',
+ 'Administrator' => 'Administrador',
+ 'Sign in' => 'Entrar',
+ 'Users' => 'Usuários',
+ 'No user' => 'Sem usuário',
+ 'Forbidden' => 'Proibido',
+ 'Access Forbidden' => 'Acesso negado',
+ 'Only administrators can access to this page.' => 'Somente administradores têm acesso a esta página.',
+ 'Edit user' => 'Editar usuário',
+ 'Logout' => 'Sair',
+ 'Bad username or password' => 'Usuário ou senha inválidos',
+ 'users' => 'usuários',
+ 'projects' => 'projetos',
+ 'Edit project' => 'Editar projeto',
+ 'Name' => 'Nome',
+ 'Activated' => 'Ativado',
+ 'Projects' => 'Projetos',
+ 'No project' => 'Nenhum projeto',
+ 'Project' => 'Projeto',
+ 'Status' => 'Status',
+ 'Tasks' => 'Tarefas',
+ 'Board' => 'Board',
+ 'Actions' => 'Ações',
+ 'Inactive' => 'Inativo',
+ 'Active' => 'Ativo',
+ 'Column %d' => 'Coluna %d',
+ 'Add this column' => 'Adicionar esta coluna',
+ '%d tasks on the board' => '%d tarefas no board',
+ '%d tasks in total' => '%d tarefas no total',
+ 'Unable to update this board.' => 'Não foi possível atualizar este board.',
+ 'Edit board' => 'Editar board',
+ 'Disable' => 'Desativar',
+ 'Enable' => 'Ativar',
+ 'New project' => 'Novo projeto',
+ 'Do you really want to remove this project: "%s"?' => 'Você realmente deseja remover este projeto: "%s" ?',
+ 'Remove project' => 'Remover projeto',
+ 'Boards' => 'Boards',
+ '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',
+ 'Add Column' => 'Adicionar Coluna',
+ 'Project "%s"' => 'Projeto "%s"',
+ 'Nobody assigned' => 'Ninguém designado',
+ 'Assigned to %s' => 'Designado para %s',
+ 'Remove a column' => 'Remover uma coluna',
+ 'Remove a column from a board' => 'Remover uma coluna do board',
+ 'Unable to remove this column.' => 'Não foi possível remover esta coluna.',
+ 'Do you really want to remove this column: "%s"?' => 'Você realmente deseja remover esta coluna: "%s"?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Esta ação irá REMOVER TODAS AS TAREFAS associadas a esta coluna!',
+ 'Settings' => 'Configurações',
+ 'Application settings' => 'Configurações da aplicação',
+ 'Language' => 'Idioma',
+ 'Webhook token:' => 'Token de webhooks:',
+ 'API token:' => 'API Token:',
+ 'More information' => 'Mais informações',
+ 'Database size:' => 'Tamanho do banco de dados:',
+ 'Download the database' => 'Download do banco de dados',
+ 'Optimize the database' => 'Otimizar o banco de dados',
+ '(VACUUM command)' => '(Comando VACUUM)',
+ '(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)',
+ 'User settings' => 'Configurações do usuário',
+ 'My default project:' => 'Meu projeto padrão:',
+ 'Close a task' => 'Finalizar uma tarefa',
+ 'Do you really want to close this task: "%s"?' => 'Você realmente deseja finalizar esta tarefa: "%s"?',
+ 'Edit a task' => 'Editar uma tarefa',
+ 'Column' => 'Coluna',
+ 'Color' => 'Cor',
+ 'Assignee' => 'Designação',
+ 'Create another task' => 'Criar outra tarefa',
+ 'New task' => 'Nova tarefa',
+ '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.',
+ 'Add a new task' => 'Adicionar uma nova tarefa',
+ 'The username is required' => 'O nome de usuário é obrigatório',
+ 'The maximum length is %d characters' => 'O tamanho máximo é %d caracteres',
+ 'The minimum length is %d characters' => 'O tamanho mínimo é %d caracteres',
+ 'The password is required' => 'A senha é obrigatória',
+ 'This value must be an integer' => 'O valor deve ser um número inteiro',
+ 'The username must be unique' => 'O nome de usuário deve ser único',
+ 'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico',
+ 'The user id is required' => 'O ID de usuário é obrigatório',
+ 'Passwords don\'t match' => 'As senhas não coincidem',
+ 'The confirmation is required' => 'A confirmação é obrigatória',
+ 'The column is required' => 'A coluna é obrigatória',
+ 'The project is required' => 'O projeto é obrigatório',
+ 'The color is required' => 'A cor é obrigatória',
+ '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',
+ 'The language is required' => 'O idioma é obrigatório',
+ 'There is no active project, the first step is to create a new project.' => 'Não há projeto ativo. O primeiro passo é criar um novo projeto.',
+ 'Settings saved successfully.' => 'Configurações salvas com sucesso.',
+ 'Unable to save your settings.' => 'Não é possível salvar suas configurações.',
+ 'Database optimization done.' => 'Otimização do banco de dados finalizada.',
+ 'Your project have been created successfully.' => 'Seu projeto foi criado com sucesso.',
+ 'Unable to create your project.' => 'Não é possível criar o seu projeto.',
+ 'Project updated successfully.' => 'Projeto atualizado com sucesso.',
+ 'Unable to update this project.' => 'Não é possível atualizar este projeto.',
+ 'Unable to remove this project.' => 'Não é possível remover este projeto.',
+ 'Project removed successfully.' => 'Projeto removido com sucesso.',
+ 'Project activated successfully.' => 'Projeto ativado com sucesso.',
+ 'Unable to activate this project.' => 'Não é possível ativar este projeto.',
+ 'Project disabled successfully.' => 'Projeto desativado com sucesso.',
+ 'Unable to disable this project.' => 'Não é possível desativar este projeto.',
+ 'Unable to open this task.' => 'Não é possível abrir esta tarefa.',
+ 'Task opened successfully.' => 'Tarefa aberta com sucesso.',
+ 'Unable to close this task.' => 'Não é possível finalizar esta tarefa.',
+ 'Task closed successfully.' => 'Tarefa finalizada com sucesso.',
+ 'Unable to update your task.' => 'Não é possível atualizar a sua tarefa.',
+ 'Task updated successfully.' => 'Tarefa atualizada com sucesso.',
+ 'Unable to create your task.' => 'Não é possível criar a sua tarefa.',
+ 'Task created successfully.' => 'Tarefa criada com sucesso.',
+ 'User created successfully.' => 'Usuário criado com sucesso.',
+ 'Unable to create your user.' => 'Não é possível criar o seu usuário.',
+ 'User updated successfully.' => 'Usuário atualizado com sucesso.',
+ 'Unable to update your user.' => 'Não é possível atualizar o seu usuário.',
+ 'User removed successfully.' => 'Usuário removido com sucesso.',
+ 'Unable to remove this user.' => 'Não é possível remover este usuário.',
+ 'Board updated successfully.' => 'Board atualizado com sucesso.',
+ 'Ready' => 'Pronto',
+ 'Backlog' => 'Backlog',
+ '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',
+ 'No task' => 'Nenhuma tarefa',
+ 'Completed tasks' => 'Tarefas completadas',
+ 'List of projects' => 'Lista de projetos',
+ 'Completed tasks for "%s"' => 'Tarefas completadas por "%s"',
+ '%d closed tasks' => '%d tarefas finalizadas',
+ 'No task for this project' => 'Não há tarefa para este projeto',
+ 'Public link' => 'Link público',
+ 'There is no column in your project!' => 'Não há colunas no seu projeto!',
+ 'Change assignee' => 'Mudar a designação',
+ 'Change assignee for the task "%s"' => 'Modificar 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',
+ 'limit' => 'limite',
+ 'Task limit' => 'Limite da tarefa',
+ 'Task count' => 'Número de tarefas',
+ 'This value must be greater than %d' => 'Este valor deve ser maior que %d',
+ 'Edit project access list' => 'Editar lista de acesso ao projeto',
+ 'Edit users access' => 'Editar acesso de usuários',
+ 'Allow this user' => 'Permitir este usuário',
+ 'Only those users have access to this project:' => 'Somente esses usuários têm acesso a este projeto:',
+ '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.',
+ 'You are not allowed to access to this project.' => 'Você não está autorizado a acessar este projeto.',
+ 'Comments' => 'Comentários',
+ 'Post comment' => 'Postar comentário',
+ 'Write your text in Markdown' => 'Escreva seu texto em Markdown',
+ 'Leave a comment' => 'Deixe um comentário',
+ 'Comment is required' => 'Comentário é obrigatório',
+ 'Leave a description' => 'Deixe uma descrição',
+ 'Comment added successfully.' => 'Comentário adicionado com sucesso.',
+ 'Unable to create your comment.' => 'Não é possível criar o seu comentário.',
+ 'The description is required' => 'A descrição é obrigatória',
+ '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.',
+ 'Remove an action' => 'Remover uma ação',
+ 'Unable to remove this action.' => 'Não é possível remover esta ação.',
+ 'Action removed successfully.' => 'Ação removida com sucesso.',
+ 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"',
+ 'Defined actions' => 'Ações definidas',
+ 'Add an action' => 'Adicionar Ação',
+ 'Event name' => 'Nome do evento',
+ 'Action name' => 'Nome da ação',
+ 'Action parameters' => 'Parâmetros da ação',
+ 'Action' => 'Ação',
+ 'Event' => 'Evento',
+ 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer execute a ação correspondente.',
+ 'Next step' => 'Próximo passo',
+ 'Define action parameters' => 'Definir parêmetros da ação',
+ 'Save this action' => 'Salvar esta ação',
+ 'Do you really want to remove this action: "%s"?' => 'Você realmente deseja remover esta ação: "%s"?',
+ 'Remove an automatic action' => 'Remover uma ação automática',
+ 'Close the task' => 'Finalizar tarefa',
+ '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',
+ 'Move a task to another column' => 'Mover a tarefa para outra coluna',
+ 'Move a task to another position in the same column' => 'Mover a tarefa para outra posição na mesma coluna',
+ 'Task modification' => 'Modificação de tarefa',
+ 'Task creation' => 'Criação de tarefa',
+ 'Open a closed task' => 'Reabrir uma tarefa finalizada',
+ 'Closing a task' => 'Finalizando uma tarefa',
+ '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',
+ 'Update this comment' => 'Atualizar este comentário',
+ 'Comment updated successfully.' => 'Comentário atualizado com sucesso.',
+ 'Unable to update your comment.' => 'Não é possível atualizar o seu comentário.',
+ 'Remove a comment' => 'Remover um comentário',
+ '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.',
+ 'Details' => 'Detalhes',
+ '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',
+ 'Reset all tokens' => 'Resetar todos os tokens',
+ 'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente.',
+ 'Unknown' => 'Desconhecido',
+ 'Last logins' => 'Últimos logins',
+ 'Login date' => 'Data de login',
+ 'Authentication method' => 'Método de autenticação',
+ 'IP address' => 'Endereço IP',
+ 'User agent' => 'User Agent',
+ 'Persistent connections' => 'Conexões persistentes',
+ 'No session.' => 'Nenhuma sessão.',
+ 'Expiration date' => 'Data de expiração',
+ 'Remember Me' => 'Lembre-se de mim',
+ 'Creation date' => 'Data de criação',
+ 'Filter by user' => 'Filtrar por usuário',
+ 'Filter by due date' => 'Filtrar por data de vencimento',
+ 'Everybody' => 'Todos',
+ 'Open' => 'Abrir',
+ 'Closed' => 'Finalizado',
+ 'Search' => 'Pesquisar',
+ 'Nothing found.' => 'Nada foi encontrado.',
+ 'Search in the project "%s"' => 'Pesquisar no projeto "%s"',
+ 'Due date' => 'Data de vencimento',
+ 'Others formats accepted: %s and %s' => 'Outros formatos permitidos: %s e %s',
+ 'Description' => 'Descrição',
+ '%d comments' => '%d comentários',
+ '%d comment' => '%d comentário',
+ 'Email address invalid' => 'Endereço de e-mail inválido',
+ 'Your Google Account is not linked anymore to your profile.' => 'Sua conta do Google não está mais associada ao seu perfil.',
+ 'Unable to unlink your Google Account.' => 'Não foi possível desassociar a sua Conta do Google.',
+ 'Google authentication failed' => 'Autenticação do Google falhou.',
+ 'Unable to link your Google Account.' => 'Não foi possível associar a sua Conta do Google.',
+ 'Your Google Account is linked to your profile successfully.' => 'Sua Conta do Google foi associada ao seu perfil com sucesso.',
+ '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 #%d' => 'Tarefa #%d',
+ '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"',
+ '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',
+ 'Category' => 'Categoria',
+ 'Category:' => 'Categoria:',
+ 'Categories' => 'Categorias',
+ 'Category not found.' => 'Categoria não encontrada.',
+ 'Your category have been created successfully.' => 'Sua categoria foi criada com sucesso.',
+ 'Unable to create your category.' => 'Não foi possível criar a sua categoria.',
+ '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.',
+ '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',
+ 'Categories for the project "%s"' => 'Categorias para o projeto "%s"',
+ 'Add a new category' => 'Adicionar uma nova categoria',
+ 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"',
+ 'Filter by category' => 'Filtrar por categoria',
+ 'All categories' => 'Todas as categorias',
+ 'No category' => 'Nenhum categoria',
+ 'The name is required' => 'O nome é obrigatório',
+ 'Remove a file' => 'Remover um arquivo',
+ '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"',
+ 'open' => 'Aberto',
+ 'Attachments' => 'Anexos',
+ 'Edit the task' => 'Editar a tarefa',
+ 'Edit the description' => 'Editar a descrição',
+ 'Add a comment' => 'Adicionar um comentário',
+ 'Edit a comment' => 'Editar um comentário',
+ 'Summary' => 'Resumo',
+ 'Time tracking' => 'Rastreamento de tempo',
+ 'Estimate:' => 'Estimado:',
+ 'Spent:' => 'Gasto:',
+ 'Do you really want to remove this sub-task?' => 'Você realmente deseja remover esta subtarefa?',
+ 'Remaining:' => 'Restante:',
+ 'hours' => 'horas',
+ 'spent' => 'gasto',
+ 'estimated' => 'estimado',
+ 'Sub-Tasks' => 'Subtarefas',
+ 'Add a sub-task' => 'Adicionar uma subtarefa',
+ 'Original estimate' => 'Estimativa original',
+ 'Create another sub-task' => 'Criar uma outra subtarefa',
+ 'Time spent' => 'Tempo gasto',
+ 'Edit a sub-task' => 'Editar uma subtarefa',
+ 'Remove a sub-task' => 'Remover uma subtarefa',
+ 'The time must be a numeric value' => 'O tempo deve ser um valor numérico',
+ 'Todo' => 'À fazer',
+ 'In progress' => 'Em andamento',
+ 'Sub-task removed successfully.' => 'Subtarefa removida com sucesso.',
+ 'Unable to remove this sub-task.' => 'Não foi possível remover esta subtarefa.',
+ 'Sub-task updated successfully.' => 'Subtarefa atualizada com sucesso.',
+ '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:',
+ 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.',
+ 'Display another project' => 'Exibir outro projeto',
+ 'Your GitHub account was successfully linked to your profile.' => 'A sua Conta do GitHub foi associada com sucesso ao seu perfil.',
+ 'Unable to link your GitHub Account.' => 'Não foi possível associar sua Conta do GitHub.',
+ 'GitHub authentication failed' => 'Autenticação do GitHub falhou',
+ 'Your GitHub account is no longer linked to your profile.' => 'A sua Conta do GitHub não está mais associada ao seu perfil.',
+ 'Unable to unlink your GitHub Account.' => 'Não foi possível desassociar a sua Conta do GitHub.',
+ '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',
+ 'End Date' => 'Data final',
+ 'Execute' => 'Executar',
+ 'Task Id' => 'ID da Tarefa',
+ 'Creator' => 'Criado por',
+ 'Modification date' => 'Data da modificação',
+ 'Completion date' => 'Data da finalização',
+ 'Clone' => 'Clonar',
+ 'Clone Project' => 'Clonar Projeto',
+ 'Project cloned successfully.' => 'Projeto clonado com sucesso.',
+ 'Unable to clone this project.' => 'Não foi possível clonar este projeto.',
+ 'Email notifications' => 'Notificações por email',
+ 'Enable email notifications' => 'Habilitar notificações por email',
+ 'Task position:' => 'Posição da tarefa:',
+ 'The task #%d have been opened.' => 'A tarefa #%d foi aberta.',
+ 'The task #%d have been closed.' => 'A tarefa #%d foi finalizada.',
+ 'Sub-task updated' => 'Subtarefa atualizada',
+ 'Title:' => 'Título:',
+ 'Status:' => 'Status:',
+ 'Assignee:' => 'Designado:',
+ 'Time tracking:' => 'Controle de tempo:',
+ 'New sub-task' => 'Nova subtarefa',
+ 'New attachment added "%s"' => 'Novo anexo adicionado "%s"',
+ 'Comment updated' => 'Comentário atualizado',
+ 'New comment posted by %s' => 'Novo comentário postado por %s',
+ 'List of due tasks for the project "%s"' => 'Lista de tarefas pendentes para o projeto "%s"',
+ 'New attachment' => 'Novo anexo',
+ 'New comment' => 'Novo comentário',
+ 'New subtask' => 'Nova subtarefa',
+ 'Subtask updated' => 'Subtarefa alterada',
+ 'Task updated' => 'Tarefa alterada',
+ 'Task closed' => 'Tarefa finalizada',
+ 'Task opened' => 'Tarefa aberta',
+ '[%s][Due tasks]' => '[%s][Tarefas pendentes]',
+ '[Kanboard] Notification' => '[Kanboard] Notificação',
+ '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',
+ 'Category management' => 'Gerenciamento de categorias',
+ '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',
+ 'Active projects' => 'Projetos ativos',
+ 'Inactive projects' => 'Projetos inativos',
+ 'Public access disabled' => 'Acesso público desabilitado',
+ 'Do you really want to disable this project: "%s"?' => 'Você realmente deseja desabilitar este projeto: "%s"?',
+ 'Do you really want to duplicate this project: "%s"?' => 'Você realmente deseja duplicar este projeto: "%s"?',
+ 'Do you really want to enable this project: "%s"?' => 'Você realmente deseja habilitar este projeto: "%s"?',
+ 'Project activation' => 'Ativação do projeto',
+ 'Move the task to another project' => 'Mover a tarefa para outro projeto',
+ 'Move to another project' => 'Mover para outro projeto',
+ 'Do you really want to duplicate this task?' => 'Você realmente deseja duplicar esta tarefa?',
+ 'Duplicate a task' => 'Duplicar uma tarefa',
+ 'External accounts' => 'Contas externas',
+ 'Account type' => 'Tipo de conta',
+ 'Local' => 'Local',
+ 'Remote' => 'Remoto',
+ 'Enabled' => 'Habilitado',
+ 'Disabled' => 'Desabilitado',
+ 'Google account linked' => 'Conta do Google associada',
+ 'Github account linked' => 'Conta do Github associada',
+ 'Username:' => 'Usuário:',
+ 'Name:' => 'Nome:',
+ 'Email:' => 'E-mail:',
+ 'Default project:' => 'Projeto padrão:',
+ '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.',
+ 'Change category for the task "%s"' => 'Mudar categoria da tarefa "%s"',
+ 'Change category' => 'Mudar categoria',
+ '%s updated the task %s' => '%s atualizou a tarefa %s',
+ '%s opened the task %s' => '%s abriu a tarefa %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s moveu a tarefa %s para a posição #%d na coluna "%s"',
+ '%s moved the task %s to the column "%s"' => '%s moveu a tarefa %s para a coluna "%s"',
+ '%s created the task %s' => '%s criou a tarefa %s',
+ '%s closed the task %s' => '%s finalizou a tarefa %s',
+ '%s created a subtask for the task %s' => '%s criou uma subtarefa para a tarefa %s',
+ '%s updated a subtask for the task %s' => '%s atualizou uma subtarefa da tarefa %s',
+ 'Assigned to %s with an estimate of %s/%sh' => 'Designado para %s com tempo estimado de %s/%sh',
+ 'Not assigned, estimate of %sh' => 'Não designado, estimado em %sh',
+ '%s updated a comment on the task %s' => '%s atualizou o comentário na tarefa %s',
+ '%s commented the task %s' => '%s comentou a tarefa %s',
+ '%s\'s activity' => 'Atividades de%s',
+ 'No activity.' => 'Sem atividade.',
+ 'RSS feed' => 'Feed RSS',
+ '%s updated a comment on the task #%d' => '%s atualizou um comentário sobre a tarefa #%d',
+ '%s commented on the task #%d' => '%s comentou sobre a tarefa #%d',
+ '%s updated a subtask for the task #%d' => '%s atualizou uma subtarefa para a tarefa #%d',
+ '%s created a subtask for the task #%d' => '%s criou uma subtarefa para a tarefa #%d',
+ '%s updated the task #%d' => '%s atualizou a tarefa #%d',
+ '%s created the task #%d' => '%s criou a tarefa #%d',
+ '%s closed the task #%d' => '%s finalizou a tarefa #%d',
+ '%s open the task #%d' => '%s abriu a tarefa #%d',
+ '%s moved the task #%d to the column "%s"' => '%s moveu a tarefa #%d para a coluna "%s"',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s moveu a tarefa #%d para a posição %d na coluna "%s"',
+ 'Activity' => 'Atividade',
+ 'Default values are "%s"' => 'Os valores padrão são "%s"',
+ 'Default columns for new projects (Comma-separated)' => 'Colunas padrão para novos projetos (Separado por vírgula)',
+ 'Task assignee change' => 'Mudar designação da tarefa',
+ '%s change the assignee of the task #%d to %s' => '%s mudou a designação da tarefa #%d para %s',
+ '%s changed the assignee of the task %s to %s' => '%s mudou a designação da tarefa %s para %s',
+ 'Column Change' => 'Mudança de coluna',
+ 'Position Change' => 'Mudança de posição',
+ 'Assignee Change' => 'Mudança de designado',
+ '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',
+ 'URL and token' => 'URL e token',
+ 'Webhook settings' => 'Configurações do Webhook',
+ 'URL for task creation:' => 'URL para a criação da tarefa:',
+ 'Reset token' => 'Resetar token',
+ 'API endpoint:' => 'API endpoint:',
+ 'Refresh interval for private board' => 'Intervalo de atualização para um board privado',
+ 'Refresh interval for public board' => 'Intervalo de atualização para um board público',
+ 'Task highlight period' => 'Período de Tarefa em destaque',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Período (em segundos) para considerar que uma tarefa foi modificada recentemente (0 para desativar, 2 dias por padrão)',
+ '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 ',
+ '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ê.',
+ 'My tasks' => 'Minhas tarefas',
+ 'Activity stream' => 'Atividades Recentes',
+ 'Dashboard' => 'Painel de Controle',
+ 'Confirmation' => 'Confirmação',
+ 'Allow everybody to access to this project' => 'Permitir que todos acessem este projeto',
+ 'Everybody have access to this project.' => 'Todos possuem acesso a este projeto.',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Integration' => 'Integração',
+ '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' => 'Github issue comment created',
+ 'Configure' => 'Configurar',
+ 'Project management' => 'Gerenciamento de projetos',
+ 'My projects' => 'Meus projetos',
+ 'Columns' => 'Colunas',
+ 'Task' => 'Tarefas',
+ 'Your are not member of any project.' => 'Você não é membro de nenhum projeto.',
+ 'Percentage' => 'Porcentagem',
+ 'Number of tasks' => 'Número de tarefas',
+ 'Task distribution' => 'Distribuição de tarefas',
+ 'Reportings' => 'Relatórios',
+ 'Task repartition for "%s"' => 'Redistribuição da tarefa para "%s"',
+ 'Analytics' => 'Estatísticas',
+ 'Subtask' => 'Subtarefa',
+ 'My subtasks' => 'Minhas subtarefas',
+ 'User repartition' => 'Redistribuição de usuário',
+ 'User repartition for "%s"' => 'Redistribuição de usuário para "%s"',
+ 'Clone this project' => 'Clonar este projeto',
+ 'Column removed successfully.' => 'Coluna removida com sucesso.',
+ 'Edit Project' => 'Editar projeto',
+ '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',
+ 'The project id must be an integer' => 'O ID do projeto deve ser um inteiro',
+ 'The status must be an integer' => 'O status deve ser um número inteiro',
+ 'The subtask id is required' => 'O ID da subtarefa é obrigatório',
+ 'The subtask id must be an integer' => 'O ID da subtarefa deve ser um número inteiro',
+ 'The task id is required' => 'O ID da tarefa é obrigatório',
+ 'The task id must be an integer' => 'O ID da tarefa deve ser um número inteiro',
+ 'The user id must be an integer' => 'O ID do usuário deve ser um número inteiro',
+ 'This value is required' => 'Este valor é obrigatório',
+ 'This value must be numeric' => 'Este valor deve ser numérico',
+ 'Unable to create this task.' => 'Não foi possível criar esta tarefa.',
+ 'Cumulative flow diagram' => 'Fluxograma cumulativo',
+ 'Cumulative flow diagram for "%s"' => 'Fluxograma cumulativo para "%s"',
+ 'Daily project summary' => 'Resumo diário do projeto',
+ 'Daily project summary export' => 'Exportação diária do resumo do projeto',
+ 'Daily project summary export for "%s"' => 'Exportação diária do resumo do projeto para "%s"',
+ 'Exports' => 'Exportar',
+ 'This export contains the number of tasks per column grouped per day.' => 'Esta exportação contém o número de tarefas por coluna agrupada por dia.',
+ 'Nothing to preview...' => 'Nada para pré-visualizar...',
+ 'Preview' => 'Pré-visualizar',
+ 'Write' => 'Escrever',
+ 'Active swimlanes' => 'Ativar swimlanes',
+ 'Add a new swimlane' => 'Adicionar novo swimlane',
+ 'Change default swimlane' => 'Alterar swimlane padrão',
+ 'Default swimlane' => 'Swimlane padrão',
+ 'Do you really want to remove this swimlane: "%s"?' => 'Você realmente deseja remover este swimlane: "%s"?',
+ 'Inactive swimlanes' => 'Desativar swimlanes',
+ 'Set project manager' => 'Definir gerente do projeto',
+ 'Set project member' => 'Definir membro do projeto',
+ 'Remove a swimlane' => 'Remover um 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 encontrado.',
+ 'Swimlane removed successfully.' => 'Swimlane removido com sucesso.',
+ '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 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 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 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',
+ 'Language:' => 'Idioma',
+ 'Timezone:' => 'Fuso horário',
+ 'All columns' => 'Todas as colunas',
+ 'Calendar for "%s"' => 'Calendário para "%s"',
+ 'Filter by column' => 'Filtrar por coluna',
+ 'Filter by status' => 'Filtrar por status',
+ 'Calendar' => 'Calendário',
+ 'Next' => 'Próximo',
+ // '#%d' => '',
+ 'Filter by color' => 'Filtrar por cor',
+ 'Filter by swimlane' => 'Filtrar por swimlane',
+ 'All swimlanes' => 'Todos os swimlane',
+ 'All colors' => 'Todas as cores',
+ 'All status' => 'Todos os status',
+ 'Add a comment logging moving the task between columns' => 'Adicionar un comentário de log ao mover uma tarefa em outra coluna',
+ 'Moved to column %s' => 'Mover para a coluna %s',
+ 'Change description' => 'Modificar a descrição',
+ 'User dashboard' => 'Painel de Controle do usuário',
+ 'Allow only one subtask in progress at the same time for a user' => 'Permitir apenas uma subtarefa em andamento ao mesmo tempo para um usuário',
+ 'Edit column "%s"' => 'Editar a coluna "%s"',
+ 'Enable time tracking for subtasks' => 'Ativar a gestão de tempo par a subtarefa',
+ 'Select the new status of the subtask: "%s"' => 'Selecionar um novo status para a subtarefa',
+ 'Subtask timesheet' => 'Gestão de tempo das subtarefas',
+ '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 as partes do projeto você deseja duplicar?',
+ 'Change dashboard view' => 'Alterar a vista do Painel de Controle',
+ 'Show/hide activities' => 'Mostrar / ocultar as atividades',
+ 'Show/hide projects' => 'Mostrar / ocultar os projetos',
+ 'Show/hide subtasks' => 'Mostrar / ocultar as subtarefas',
+ 'Show/hide tasks' => 'Mostrar / ocultar as tarefas',
+ 'Disable login form' => 'Desativar o formulário de login',
+ 'Show/hide calendar' => 'Mostrar / ocultar calendário',
+ 'User calendar' => 'Calendário do usuário',
+ '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',
+ 'Days in this column' => 'Dias nesta coluna',
+ // '%dd' => '',
+ 'Add a link' => 'Adicionar uma associação',
+ 'Add a new link' => 'Adicionar uma nova associação',
+ 'Do you really want to remove this link: "%s"?' => '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?',
+ 'Field required' => 'Campo requerido',
+ 'Link added successfully.' => 'Associação criada com sucesso.',
+ 'Link updated successfully.' => 'Associação atualizada com sucesso.',
+ 'Link removed successfully.' => 'Associação removida com sucesso.',
+ 'Link labels' => 'Etiquetas das associações',
+ 'Link modification' => 'Modificação de uma associação',
+ 'Links' => 'Associações',
+ 'Link settings' => 'Configuração das associações',
+ 'Opposite label' => 'Nome da etiqueta oposta',
+ 'Remove a link' => 'Remover uma associação',
+ 'Task\'s links' => 'Associações das tarefas',
+ 'The labels must be different' => 'As etiquetas devem ser diferentes',
+ 'There is no link.' => 'Não há nenhuma associação.',
+ 'This label must be unique' => 'Esta etiqueta deve ser unica',
+ 'Unable to create your link.' => 'Impossível de adicionar sua associação.',
+ 'Unable to update your link.' => 'Impossível de atualizar sua associação.',
+ 'Unable to remove this link.' => 'Impossível de remover sua associação.',
+ 'relates to' => 'é associado com',
+ 'blocks' => 'blocos',
+ 'is blocked by' => 'esta bloqueado por',
+ 'duplicates' => 'duplica',
+ 'is duplicated by' => 'é duplicado por',
+ 'is a child of' => 'é um filho de',
+ 'is a parent of' => 'é um parente do',
+ 'targets milestone' => 'visa um milestone',
+ 'is a milestone of' => 'é um milestone de',
+ 'fixes' => 'corrige',
+ 'is fixed by' => 'foi corrigido por',
+ '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',
+ 'Close dialog box' => 'Fechar a caixa de diálogo',
+ 'Submit a form' => 'Envia o formulário',
+ 'Board view' => 'Página do painel',
+ 'Keyboard shortcuts' => 'Atalhos de teclado',
+ 'Open board switcher' => 'Abrir o comutador de painel',
+ 'Application' => 'Aplicação',
+ 'Filter recently updated' => 'Filtro recentemente atualizado',
+ 'since %B %e, %Y at %k:%M %p' => 'desde o %d/%m/%Y às %H:%M',
+ 'More filters' => 'Mais filtros',
+ 'Compact view' => 'Vista reduzida',
+ 'Horizontal scrolling' => 'Rolagem horizontal',
+ 'Compact/wide view' => 'Alternar entre a vista compacta e ampliada',
+ 'No results match:' => 'Nenhum resultado:',
+ 'Remove hourly rate' => 'Retirar taxa horária',
+ 'Do you really want to remove this hourly rate?' => 'Você deseja realmente remover esta taxa horária?',
+ 'Hourly rates' => 'Taxas horárias',
+ 'Hourly rate' => 'Taxa horária',
+ 'Currency' => 'Moeda',
+ 'Effective date' => 'Data efetiva',
+ 'Add new rate' => 'Adicionar nova taxa',
+ 'Rate removed successfully.' => 'Taxa removido com sucesso.',
+ 'Unable to remove this rate.' => 'Impossível de remover esta taxa.',
+ 'Unable to save the hourly rate.' => 'Impossível salvar a taxa horária.',
+ 'Hourly rate created successfully.' => 'Taxa horária criada com sucesso.',
+ 'Start time' => 'Horário de início',
+ 'End time' => 'Horário de término',
+ 'Comment' => 'comentário',
+ 'All day' => 'Dia inteiro',
+ 'Day' => 'Dia',
+ 'Manage timetable' => 'Gestão dos horários',
+ 'Overtime timetable' => 'Horas extras',
+ 'Time off timetable' => 'Horas de ausência',
+ 'Timetable' => 'Horários',
+ 'Work timetable' => 'Horas trabalhadas',
+ 'Week timetable' => 'Horário da semana',
+ 'Day timetable' => 'Horário de un dia',
+ 'From' => 'Desde',
+ 'To' => 'A',
+ 'Time slot created successfully.' => 'Intervalo de tempo criado com sucesso.',
+ 'Unable to save this time slot.' => 'Impossível de guardar este intervalo de tempo.',
+ 'Time slot removed successfully.' => 'Intervalo de tempo removido com sucesso.',
+ 'Unable to remove this time slot.' => 'Impossível de remover esse intervalo de tempo.',
+ 'Do you really want to remove this time slot?' => 'Você deseja realmente remover este intervalo de tempo?',
+ 'Remove time slot' => 'Remover um intervalo de tempo',
+ 'Add new time slot' => 'Adicionar um intervalo de tempo',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Esses horários são usados quando a caixa de seleção "Dia inteiro" está marcada para Horas de ausência ou Extras',
+ 'Files' => 'Arquivos',
+ 'Images' => 'Imagens',
+ 'Private project' => 'Projeto privado',
+ 'Amount' => 'Quantia',
+ 'AUD - Australian Dollar' => 'AUD - Dólar australiano',
+ 'Budget' => 'Orçamento',
+ 'Budget line' => 'Rubrica orçamental',
+ 'Budget line removed successfully.' => 'Rubrica orçamental removida com sucesso',
+ 'Budget lines' => 'Rubricas orçamentais',
+ 'CAD - Canadian Dollar' => 'CAD - Dólar canadense',
+ 'CHF - Swiss Francs' => 'CHF - Francos Suíços',
+ 'Cost' => 'Custo',
+ 'Cost breakdown' => 'Repartição dos custos',
+ 'Custom Stylesheet' => 'Folha de estilo personalizado',
+ 'download' => 'baixar',
+ 'Do you really want to remove this budget line?' => 'Você deseja realmente remover esta rubrica orçamental?',
+ 'EUR - Euro' => 'EUR - Euro',
+ 'Expenses' => 'Despesas',
+ 'GBP - British Pound' => 'GBP - Libra Esterlina',
+ 'INR - Indian Rupee' => 'INR - Rúpia indiana',
+ 'JPY - Japanese Yen' => 'JPY - Iene japonês',
+ 'New budget line' => 'Nova rubrica orçamental',
+ 'NZD - New Zealand Dollar' => 'NZD - Dólar Neozelandês',
+ 'Remove a budget line' => 'Remover uma rubrica orçamental',
+ 'Remove budget line' => 'Remover uma rubrica orçamental',
+ 'RSD - Serbian dinar' => 'RSD - Dinar sérvio',
+ 'The budget line have been created successfully.' => 'A rubrica orçamental foi criada com sucesso.',
+ 'Unable to create the budget line.' => 'Impossível de adicionar esta rubrica orçamental.',
+ 'Unable to remove this budget line.' => 'Impossível de remover esta rubrica orçamental.',
+ 'USD - US Dollar' => 'USD - Dólar norte-americano',
+ 'Remaining' => 'Restante',
+ 'Destination column' => 'Coluna de destino',
+ 'Move the task to another column when assigned to a user' => 'Mover a tarefa para uma outra coluna quando esta está atribuída a um usuário',
+ 'Move the task to another column when assignee is cleared' => 'Mover a tarefa para uma outra coluna quando esta não está atribuída',
+ 'Source column' => 'Coluna de origem',
+ 'Show subtask estimates (forecast of future work)' => 'Mostrar a estimativa das subtarefas (previsão para o trabalho futuro)',
+ 'Transitions' => 'Transições',
+ 'Executer' => 'Executor(a)',
+ 'Time spent in the column' => 'Tempo gasto na coluna',
+ 'Task transitions' => 'Transições das tarefas',
+ 'Task transitions export' => 'Exportação das transições das tarefas',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Este relatório contém todos os movimentos de coluna para cada tarefa com a data, o usuário e o tempo gasto para cada transição.',
+ 'Currency rates' => 'Taxas de câmbio das moedas estrangeiras',
+ 'Rate' => 'Taxa',
+ 'Change reference currency' => 'Mudar a moeda de referência',
+ 'Add a new currency rate' => 'Adicionar uma nova taxa para uma moeda',
+ 'Currency rates are used to calculate project budget.' => 'As taxas de câmbio são utilizadas para calcular o orçamento do projeto.',
+ 'Reference currency' => 'Moeda de Referência',
+ 'The currency rate have been added successfully.' => 'A taxa de câmbio foi adicionada com sucesso.',
+ 'Unable to add this currency rate.' => 'Impossível de adicionar essa taxa de câmbio.',
+ 'Send notifications to a Slack channel' => 'Enviar as notificações em um canal Slack',
+ 'Webhook URL' => 'URL do webhook',
+ 'Help on Slack integration' => 'Ajuda na integração com o Slack',
+ '%s remove the assignee of the task %s' => '%s removeu a pessoa designada para a tarefa %s',
+ 'Send notifications to Hipchat' => 'Enviar as notificações para o Hipchat',
+ 'API URL' => 'URL da API',
+ 'Room API ID or name' => 'Nome ou ID da sala de discussão',
+ 'Room notification token' => 'Código de segurança da sala de discussão',
+ 'Help on Hipchat integration' => 'Ajuda na integração com o Hipchat',
+ 'Enable Gravatar images' => 'Ativar imagem 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',
+ 'Code' => 'Código',
+ 'Two factor authentication' => 'Autenticação à fator duplo',
+ 'Enable/disable two factor authentication' => 'Ativar/Desativar autenticação à fator 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).' => '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',
+ '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.',
+ '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',
+ 'Postmark (incoming emails)' => 'Postmark (e-mails recebidos)',
+ 'Help on Postmark integration' => 'Ajuda na integração do Postmark',
+ 'Mailgun (incoming emails)' => 'Mailgun (e-mails recebidos)',
+ 'Help on Mailgun integration' => 'Ajuda na integração do Mailgun',
+ 'Sendgrid (incoming emails)' => 'Sendgrid (e-mails recebidos)',
+ 'Help on Sendgrid integration' => 'Ajuda na integração do Sendgrid',
+ '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"?',
+ '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',
+ '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',
+ 'This value must be alphanumeric' => 'Este valor deve ser alfanumérico',
+ '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',
+ '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',
+ 'Base date to calculate new due date: ' => 'Data a ser utilizada para calcular a nova data limite: ',
+ '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: ' => 'Fator para calcular a nova data limite: ',
+ '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: ',
+ 'Trigger to generate recurrent task: ' => 'Trigger 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',
+ 'When task is moved to last column' => 'Quando a tarefa é movida para a última coluna',
+ 'Year(s)' => 'Ano(s)',
+ 'Jabber (XMPP)' => 'Jabber (XMPP)',
+ 'Send notifications to Jabber' => 'Enviar notificações para o Jabber',
+ 'XMPP server address' => 'Endereço do servidor XMPP',
+ 'Jabber domain' => 'Nome de domínio Jabber',
+ 'Jabber nickname' => 'Apelido Jabber',
+ 'Multi-user chat room' => 'Sala de chat multi-usuário',
+ 'Help on Jabber integration' => 'Ajuda na integração com Jabber',
+ 'The server address must use this format: "tcp://hostname:5222"' => 'O endereço do servidor deve usar o seguinte formato: "tcp://hostname:5222"',
+ 'Calendar settings' => 'Configurações do calendário',
+ 'Project calendar view' => 'Vista em modo projeto do calendário',
+ 'Project settings' => 'Configurações dos projetos',
+ 'Show subtasks based on the time tracking' => 'Mostrar as subtarefas com base no controle de tempo',
+ 'Show tasks based on the creation date' => 'Mostrar as tarefas em função da data de criação',
+ 'Show tasks based on the start date' => 'Mostrar as tarefas em função da data de início',
+ 'Subtasks time tracking' => 'Monitoramento do tempo comparado as subtarefas',
+ 'User calendar view' => 'Vista em modo utilizador do calendário',
+ 'Automatically update the start date' => 'Atualizar automaticamente a data de início',
+ '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',
+ 'Unable to update this user.' => 'Impossível de atualizar esse usuário.',
+ // 'There is no user management for private projects.' => '',
+);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
new file mode 100644
index 00000000..ee71ea95
--- /dev/null
+++ b/app/Locale/ru_RU/translations.php
@@ -0,0 +1,925 @@
+<?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' => 'Серый',
+ 'Save' => 'Сохранить',
+ 'Login' => 'Вход',
+ 'Official website:' => 'Официальный сайт:',
+ 'Unassigned' => 'Не назначена',
+ 'View this task' => 'Посмотреть задачу',
+ 'Remove user' => 'Удалить пользователя',
+ 'Do you really want to remove this user: "%s"?' => 'Вы точно хотите удалить пользователя: « %s » ?',
+ 'New user' => 'Новый пользователь',
+ 'All users' => 'Все пользователи',
+ 'Username' => 'Имя пользователя',
+ 'Password' => 'Пароль',
+ 'Default project' => 'Проект по умолчанию',
+ 'Administrator' => 'Администратор',
+ 'Sign in' => 'Войти',
+ 'Users' => 'Пользователи',
+ 'No user' => 'Нет пользователя',
+ 'Forbidden' => 'Запрещено',
+ 'Access Forbidden' => 'Доступ запрещен',
+ 'Only administrators can access to this page.' => 'Только администраторы могут войти на эту страницу.',
+ 'Edit user' => 'Изменить пользователя',
+ 'Logout' => 'Выйти',
+ 'Bad username or password' => 'Неверное имя пользователя или пароль',
+ 'users' => 'пользователи',
+ 'projects' => 'проекты',
+ 'Edit project' => 'Изменить проект',
+ 'Name' => 'Имя',
+ 'Activated' => 'Активен',
+ 'Projects' => 'Проекты',
+ 'No project' => 'Нет проекта',
+ 'Project' => 'Проект',
+ 'Status' => 'Статус',
+ 'Tasks' => 'Задачи',
+ 'Board' => 'Доска',
+ 'Actions' => 'Действия',
+ 'Inactive' => 'Неактивен',
+ 'Active' => 'Активен',
+ 'Column %d' => 'Колонка %d',
+ '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' => 'Удалить проект',
+ 'Boards' => 'Доски',
+ 'Edit the board for "%s"' => 'Изменить доску для « %s »',
+ 'All projects' => 'Все проекты',
+ 'Change columns' => 'Изменить колонки',
+ 'Add a new column' => 'Добавить новую колонку',
+ 'Title' => 'Название',
+ 'Add Column' => 'Добавить колонку',
+ 'Project "%s"' => 'Проект « %s »',
+ '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:' => 'Webhooks токен :',
+ 'API token:' => 'API токен :',
+ 'More information' => 'Подробнее',
+ 'Database size:' => 'Размер базы данных :',
+ 'Download the database' => 'Скачать базу данных',
+ 'Optimize the database' => 'Оптимизировать базу данных',
+ '(VACUUM command)' => '(Команда VACUUM)',
+ '(Gzip compressed Sqlite file)' => '(Сжать GZip файл SQLite)',
+ 'User settings' => 'Настройки пользователя',
+ 'My default project:' => 'Мой проект по умолчанию:',
+ 'Close a task' => 'Закрыть задачу',
+ 'Do you really want to close this task: "%s"?' => 'Вы точно хотите закрыть задачу: « %s » ?',
+ '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' => 'Вернуться на доску',
+ '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.' => 'Нет описания.',
+ '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 username must be alphanumeric' => 'Имя пользователя должно быть буквенно-цифровым',
+ 'The user id is required' => 'Требуется ID пользователя',
+ 'Passwords don\'t match' => 'Пароли не совпадают',
+ 'The confirmation is required' => 'Требуется подтверждение',
+ 'The column is required' => 'Требуется колонка',
+ 'The project is required' => 'Требуется проект',
+ 'The color 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' => 'Требуется заголовок',
+ 'The language is required' => 'Требуется язык',
+ 'There is no active project, the first step is to create a new project.' => 'Нет активного проекта, сначала создайте новый проект.',
+ '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:' => 'Версия приложения:',
+ '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',
+ 'No task' => 'Нет задачи',
+ 'Completed tasks' => 'Завершенные задачи',
+ 'List of projects' => 'Список проектов',
+ 'Completed tasks for "%s"' => 'Завершенные задачи для « %s »',
+ '%d closed tasks' => '%d завершенных задач',
+ 'No task for this project' => 'Нет задач для этого проекта',
+ 'Public link' => 'Ссылка для просмотра',
+ 'There is no column in your project!' => 'Нет колонки в вашем проекте!',
+ 'Change assignee' => 'Сменить назначенного',
+ 'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »',
+ 'Timezone' => 'Часовой пояс',
+ 'Sorry, I didn\'t find this information in my database!' => 'К сожалению, информация в базе данных не найдена !',
+ 'Page not found' => 'Страница не найдена',
+ 'Complexity' => 'Сложность',
+ 'limit' => 'лимит',
+ 'Task limit' => 'Лимит задач',
+ 'Task count' => 'Количество задач',
+ 'This value must be greater than %d' => 'Это значение должно быть больше %d',
+ 'Edit project access list' => 'Изменить доступ к проекту',
+ 'Edit users access' => 'Изменить доступ пользователей',
+ 'Allow this user' => 'Разрешить этого пользователя',
+ 'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту:',
+ 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет неограниченные права.',
+ 'Revoke' => 'Отозвать',
+ 'List of authorized users' => 'Список авторизованных пользователей',
+ 'User' => 'Пользователь',
+ 'Nobody have access to this project.' => 'Ни у кого нет доступа к этому проекту',
+ 'You are not allowed to access to this project.' => 'Вам запрещен доступ к этому проекту.',
+ 'Comments' => 'Комментарии',
+ 'Post comment' => 'Оставить комментарий',
+ 'Write your text in Markdown' => 'Справка по синтаксису Markdown',
+ 'Leave a comment' => 'Оставить комментарий 2',
+ 'Comment is required' => 'Нужен комментарий',
+ 'Leave a description' => 'Напишите описание',
+ 'Comment added successfully.' => 'Комментарий успешно добавлен.',
+ 'Unable to create your comment.' => 'Невозможно создать комментарий.',
+ 'The description is required' => 'Требуется описание',
+ '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.' => 'Не удалось создать автоматизированное действие.',
+ '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' => 'Удалить автоматическое действие',
+ 'Close the task' => 'Закрыть задачу',
+ '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' => 'Переместить задачу в другую колонку',
+ 'Move a task to another position in the same column' => 'Переместить задачу в другое место этой же колонки',
+ 'Task modification' => 'Изменение задачи',
+ 'Task creation' => 'Создание задачи',
+ 'Open a closed task' => 'Открыть завершенную задачу',
+ 'Closing a task' => 'Завершение задачи',
+ 'Assign a color to a specific user' => 'Назначить определенный цвет пользователю',
+ 'Column title' => 'Название колонки',
+ 'Position' => 'Расположение',
+ 'Move Up' => 'Сдвинуть вверх',
+ 'Move Down' => 'Сдвинуть вниз',
+ 'Duplicate to another project' => 'Клонировать в другой проект',
+ 'Duplicate' => 'Клонировать',
+ 'link' => 'ссылка',
+ 'Update this comment' => 'Обновить комментарий',
+ '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?' => 'Вы точно хотите удалить этот комментарий?',
+ 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор и автор комментария имеют доступ к этой странице.',
+ 'Details' => 'Подробности',
+ 'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »',
+ 'The current password is required' => 'Требуется текущий пароль',
+ 'Wrong password' => 'Неверный пароль',
+ 'Reset all tokens' => 'Сброс всех токенов',
+ 'All tokens have been regenerated.' => 'Все токены пересозданы.',
+ 'Unknown' => 'Неизвестно',
+ 'Last logins' => 'Последние посещения',
+ 'Login date' => 'Дата входа',
+ 'Authentication method' => 'Способ аутентификации',
+ 'IP address' => 'IP адрес',
+ 'User agent' => 'User agent',
+ 'Persistent connections' => 'Постоянные соединения',
+ 'No session.' => 'Нет сеанса',
+ 'Expiration date' => 'Дата окончания',
+ 'Remember Me' => 'Запомнить меня',
+ 'Creation date' => 'Дата создания',
+ 'Filter by user' => 'Фильтр по пользователям',
+ 'Filter by due date' => 'Фильтр по дате',
+ 'Everybody' => 'Все',
+ 'Open' => 'Открытый',
+ 'Closed' => 'Закрытый',
+ 'Search' => 'Поиск',
+ 'Nothing found.' => 'Ничего не найдено.',
+ 'Search in the project "%s"' => 'Искать в проекте « %s »',
+ 'Due date' => 'Срок',
+ 'Others formats accepted: %s and %s' => 'Другой формат приемлем: %s и %s',
+ 'Description' => 'Описание',
+ '%d comments' => '%d комментариев',
+ '%d comment' => '%d комментарий',
+ 'Email address invalid' => 'Некорректный e-mail адрес',
+ 'Your Google Account is not linked anymore to your profile.' => 'Ваш аккаунт в Google больше не привязан к вашему профилю.',
+ 'Unable to unlink your Google Account.' => 'Не удалось отвязать ваш профиль от Google.',
+ 'Google authentication failed' => 'Аутентификация Google не удалась',
+ 'Unable to link your Google Account.' => 'Не удалось привязать ваш профиль к Google.',
+ 'Your Google Account is linked to your profile successfully.' => 'Ваш профиль успешно привязан к Google.',
+ 'Email' => 'E-mail',
+ 'Link my Google Account' => 'Привязать мой профиль к Google',
+ 'Unlink my Google Account' => 'Отвязать мой профиль от Google',
+ 'Login with my Google Account' => 'Аутентификация через Google',
+ 'Project not found.' => 'Проект не найден.',
+ 'Task #%d' => 'Задача n°%d',
+ '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' => 'Название категории',
+ 'Categories for the project "%s"' => 'Категории для проекта « %s »',
+ 'Add a new category' => 'Добавить новую категорию',
+ 'Do you really want to remove this category: "%s"?' => 'Вы точно хотите удалить категорию « %s » ?',
+ 'Filter by category' => 'Фильтр по категориям',
+ '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 » ?',
+ 'open' => 'открыть',
+ 'Attachments' => 'Приложение',
+ 'Edit the task' => 'Изменить задачу',
+ 'Edit the description' => 'Изменить описание',
+ 'Add a comment' => 'Добавить комментарий',
+ 'Edit a comment' => 'Изменить комментарий',
+ 'Summary' => 'Сводка',
+ 'Time tracking' => 'Отслеживание времени',
+ 'Estimate:' => 'Приблизительно:',
+ 'Spent:' => 'Затрачено:',
+ 'Do you really want to remove this sub-task?' => 'Вы точно хотите удалить подзадачу?',
+ 'Remaining:' => 'Осталось:',
+ 'hours' => 'часов',
+ 'spent' => 'затрачено',
+ 'estimated' => 'расчетное',
+ 'Sub-Tasks' => 'Подзадачи',
+ 'Add a sub-task' => 'Добавить подзадачу',
+ 'Original estimate' => 'Первичная оценка',
+ 'Create another sub-task' => 'Создать другую подзадачу',
+ 'Time spent' => 'Времени затрачено',
+ 'Edit a sub-task' => 'Изменить подзадачу',
+ 'Remove a sub-task' => 'Удалить подзадачу',
+ 'The time must be a numeric value' => 'Время должно быть числом!',
+ 'Todo' => 'К исполнению',
+ 'In progress' => 'В процессе',
+ 'Sub-task removed successfully.' => 'Подзадача удалена.',
+ 'Unable to remove this sub-task.' => 'Не удалось удалить подзадачу.',
+ 'Sub-task updated successfully.' => 'Подзадача обновлена.',
+ 'Unable to update your sub-task.' => 'Не удалось обновить подзадачу.',
+ 'Unable to create your sub-task.' => 'Не удалось создать подзадачу.',
+ 'Sub-task added successfully.' => 'Подзадача добавлена.',
+ 'Maximum size: ' => 'Максимальный размер: ',
+ 'Unable to upload the file.' => 'Не удалось загрузить файл.',
+ 'Display another project' => 'Показать другой проект',
+ 'Your GitHub account was successfully linked to your profile.' => 'Ваш GitHub привязан к вашему профилю.',
+ 'Unable to link your GitHub Account.' => 'Не удалось привязать ваш профиль к GitHub.',
+ 'GitHub authentication failed' => 'Аутентификация в GitHub не удалась',
+ 'Your GitHub account is no longer linked to your profile.' => 'Ваш GitHub отвязан от вашего профиля.',
+ 'Unable to unlink your GitHub Account.' => 'Не удалось отвязать ваш профиль от GitHub.',
+ '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' => 'Дата начала',
+ 'End Date' => 'Дата завершения',
+ 'Execute' => 'Выполнить',
+ 'Task Id' => 'ID задачи',
+ 'Creator' => 'Автор',
+ 'Modification date' => 'Дата изменения',
+ 'Completion date' => 'Дата завершения',
+ 'Clone' => 'Клонировать',
+ 'Clone Project' => 'Клонировать проект',
+ 'Project cloned successfully.' => 'Проект клонирован.',
+ 'Unable to clone this project.' => 'Не удалось клонировать проект.',
+ 'Email notifications' => 'Уведомления по e-mail',
+ 'Enable email notifications' => 'Включить уведомления по e-mail',
+ '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 »',
+ 'List of due tasks for the project "%s"' => 'Список сроков к проекту « %s »',
+ 'New attachment' => 'Новое вложение',
+ 'New comment' => 'Новый комментарий',
+ 'New subtask' => 'Новая подзадача',
+ 'Subtask updated' => 'Подзадача обновлена',
+ 'Task updated' => 'Задача обновлена',
+ 'Task closed' => 'Задача закрыта',
+ 'Task opened' => 'Задача открыта',
+ '[%s][Due tasks]' => '[%s][Текущие задачи]',
+ '[Kanboard] Notification' => '[Kanboard] Оповещение',
+ 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам:',
+ 'view the task on Kanboard' => 'посмотреть задачу на Kanboard',
+ 'Public access' => 'Общий доступ',
+ 'Category management' => 'Управление категориями',
+ 'User management' => 'Управление пользователями',
+ 'Active tasks' => 'Активные задачи',
+ 'Disable public access' => 'Отключить общий доступ',
+ 'Enable public access' => 'Включить общий доступ',
+ 'Active projects' => 'Активные проекты',
+ 'Inactive projects' => 'Неактивные проекты',
+ 'Public access disabled' => 'Общий доступ отключен',
+ 'Do you really want to disable this project: "%s"?' => 'Вы точно хотите деактивировать проект: "%s"?',
+ 'Do you really want to duplicate 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' => 'Выключены',
+ 'Google account linked' => 'Профиль Google связан',
+ 'Github account linked' => 'Профиль GitHub связан',
+ 'Username:' => 'Имя пользователя:',
+ 'Name:' => 'Имя:',
+ 'Email:' => 'E-mail:',
+ 'Default project:' => 'Проект по умолчанию:',
+ '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.' => 'Не удалось сменить пароль.',
+ '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"',
+ '%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 активность',
+ 'No activity.' => 'Нет активности',
+ 'RSS feed' => 'RSS лента',
+ '%s updated a comment on the task #%d' => '%s обновил комментарий задачи #%d',
+ '%s commented on the task #%d' => '%s прокомментировал задачу #%d',
+ '%s updated a subtask for the task #%d' => '%s обновил подзадачу задачи #%d',
+ '%s created a subtask for the task #%d' => '%s создал подзадачу для задачи #%d',
+ '%s updated the task #%d' => '%s обновил задачу #%d',
+ '%s created the task #%d' => '%s создал задачу #%d',
+ '%s closed the task #%d' => '%s закрыл задачу #%d',
+ '%s open the task #%d' => '%s открыл задачу #%d',
+ '%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s переместил задачу #%d на позицию %d в колонке "%s"',
+ 'Activity' => 'Активность',
+ 'Default values are "%s"' => 'Колонки по умолчанию: "%s"',
+ 'Default columns for new projects (Comma-separated)' => 'Колонки по умолчанию для новых проектов (разделять запятой)',
+ 'Task assignee change' => 'Изменен назначенный',
+ '%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',
+ 'Column Change' => 'Изменение колонки',
+ 'Position Change' => 'Изменение позиции',
+ 'Assignee Change' => 'Изменение ответственного',
+ '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' => 'Информация',
+ 'Database driver:' => 'Драйвер базы данных',
+ 'Board settings' => 'Настройки доски',
+ 'URL and token' => 'URL и токен',
+ 'Webhook settings' => 'Параметры Webhook',
+ 'URL for task creation:' => 'URL для создания задачи:',
+ 'Reset token' => 'Перезагрузить токен',
+ 'API endpoint:' => 'API endpoint:',
+ 'Refresh interval for private board' => 'Период обновления для частных досок',
+ 'Refresh interval for public board' => 'Период обновления для публичных досок',
+ 'Task highlight period' => 'Время подсвечивания задачи',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Период (в секундах) в течении которого задача считается недавно измененной (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' => '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"',
+ 'New private project' => 'Новый проект с ограниченным доступом',
+ '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.' => 'Вам ничего не назначено',
+ 'My tasks' => 'Мои задачи',
+ 'Activity stream' => 'Текущая активность',
+ 'Dashboard' => 'Инфопанель',
+ 'Confirmation' => 'Подтверждение пароля',
+ 'Allow everybody to access to this project' => 'Разрешить любому',
+ 'Everybody have access to this project.' => 'Любой может получить доступ к этому проекту.',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Integration' => 'Интеграция',
+ 'Github webhooks' => 'GitHub webhooks',
+ 'Help on Github webhooks' => 'Помощь по GitHub webhooks',
+ 'Create a comment from an external provider' => 'Создать комментарий из внешнего источника',
+ 'Github issue comment created' => 'Github issue комментарий создан',
+ 'Configure' => 'Настройки',
+ '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.' => 'Колонка успешно удалена.',
+ 'Edit Project' => 'Редактировать Проект',
+ // 'Github Issue' => '',
+ 'Not enough data to show the graph.' => 'Недостаточно данных, чтобы показать график.',
+ 'Previous' => 'Предыдущий',
+ 'The id must be an integer' => 'Этот id должен быть целочисленным',
+ 'The project id must be an integer' => 'Id проекта должен быть целочисленным',
+ 'The status must be an integer' => 'Статус должен быть целочисленным',
+ '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' => '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' => 'Написание',
+ '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.' => 'Стандартная 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' => 'Экспортировать подзадачи',
+ 'Subtasks exportation for "%s"' => 'Экспорт подзадач для "%s"',
+ 'Task Title' => 'Загловок задачи',
+ 'Untitled' => 'Заголовок отсутствует',
+ 'Application default' => 'Приложение по умолчанию',
+ 'Language:' => 'Язык:',
+ 'Timezone:' => 'Временная зона:',
+ 'All columns' => 'Все колонки',
+ 'Calendar for "%s"' => 'Календарь для "%s"',
+ 'Filter by column' => 'Фильтр по колонке',
+ 'Filter by status' => 'Фильтр по статусу',
+ 'Calendar' => 'Календарь',
+ 'Next' => 'Следующий',
+ // '#%d' => '',
+ 'Filter by color' => 'Фильтрация по цвету',
+ 'Filter by swimlane' => 'Фильтрация по дорожкам',
+ 'All swimlanes' => 'Все дорожки',
+ 'All colors' => 'Все цвета',
+ 'All status' => 'Все статусы',
+ 'Add a comment logging moving the task between columns' => 'Добавлять комментарий при движении задач между колонками',
+ '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"',
+ 'Enable time tracking for subtasks' => 'Включить учет времени для подзадач',
+ '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?' => 'Какие части проекта должны быть дублированы?',
+ 'Change dashboard view' => 'Изменить отображение панели мониторинга',
+ 'Show/hide activities' => 'Показать/скрыть активность',
+ 'Show/hide projects' => 'Показать/скрыть проекты',
+ 'Show/hide subtasks' => 'Показать/скрыть подзадачи',
+ 'Show/hide tasks' => 'Показать/скрыть задачи',
+ 'Disable login form' => 'Выключить форму авторизации',
+ 'Show/hide calendar' => 'Показать/скрыть календарь',
+ 'User calendar' => 'Пользовательский календарь',
+ // 'Bitbucket commit received' => '',
+ 'Bitbucket webhooks' => 'BitBucket webhooks',
+ 'Help on Bitbucket webhooks' => 'Помощь по BitBucket webhooks',
+ 'Start' => 'Начало',
+ 'End' => 'Конец',
+ 'Task age in days' => 'Возраст задачи в днях',
+ 'Days in this column' => 'Дней в этой колонке',
+ // '%dd' => '',
+ 'Add a link' => 'Добавить ссылку на другие задачи',
+ 'Add a new link' => 'Добавление новой ссылки',
+ 'Do you really want to remove this link: "%s"?' => 'Вы уверены что хотите удалить ссылку: "%s"?',
+ 'Do you really want to remove this link with task #%d?' => 'Вы уверены что хотите удалить ссылку вместе с задачей #%d?',
+ 'Field required' => 'Поле обязательно для заполнения',
+ 'Link added successfully.' => 'Ссылка успешно добавлена',
+ 'Link updated successfully.' => 'Ссылка успешно обновлена',
+ 'Link removed successfully.' => 'Ссылка успешно удалена',
+ 'Link labels' => 'Метки для ссылки',
+ 'Link modification' => 'Обновление ссылки',
+ 'Links' => 'Ссылки',
+ 'Link settings' => 'Настройки ссылки',
+ 'Opposite label' => 'Ярлык напротив',
+ 'Remove a link' => 'Удалить ссылку',
+ 'Task\'s links' => 'Ссылки задачи',
+ 'The labels must be different' => 'Ярлыки должны быть разными',
+ 'There is no link.' => 'Это не ссылка',
+ 'This label must be unique' => 'Этот ярлык должна быть уникальной ',
+ 'Unable to create your link.' => 'Не удается создать эту ссылку.',
+ 'Unable to update your link.' => 'Не удается обновить эту ссылку.',
+ 'Unable to remove this link.' => 'Не удается удалить эту ссылку.',
+ 'relates to' => 'связана с',
+ 'blocks' => 'блокирует',
+ 'is blocked by' => 'заблокирована в',
+ 'duplicates' => 'дублирует',
+ 'is duplicated by' => 'дублирована в',
+ 'is a child of' => 'наследник',
+ 'is a parent of' => 'родитель',
+ 'targets milestone' => 'часть этапа',
+ 'is a milestone of' => 'является частью этапа',
+ 'fixes' => 'исправляет',
+ 'is fixed by' => 'исправлено в',
+ 'This task' => 'Эта задача',
+ '<1h' => '<1ч',
+ // '%dh' => '',
+ // '%b %e' => '',
+ 'Expand tasks' => 'Развернуть задачи',
+ 'Collapse tasks' => 'Свернуть задачи',
+ 'Expand/collapse tasks' => 'Развернуть/свернуть задачи',
+ 'Close dialog box' => 'Закрыть диалог',
+ 'Submit a form' => 'Отправить форму',
+ 'Board view' => 'Просмотр доски',
+ 'Keyboard shortcuts' => 'Горячие клавиши',
+ 'Open board switcher' => 'Открыть переключатель доски',
+ 'Application' => 'Приложение',
+ 'Filter recently updated' => 'Сортировать по дате обновления',
+ // 'since %B %e, %Y at %k:%M %p' => '',
+ 'More filters' => 'Дополнительные фильтры',
+ 'Compact view' => 'Компактный вид',
+ 'Horizontal scrolling' => 'Широкий вид',
+ 'Compact/wide view' => 'Компактный/широкий вид',
+ 'No results match:' => 'Отсутствуют результаты:',
+ 'Remove hourly rate' => 'Удалить почасовую ставку',
+ 'Do you really want to remove this hourly rate?' => 'Вы действительно хотите удалить эту почасовую ставку?',
+ 'Hourly rates' => 'Почасовые ставки',
+ 'Hourly rate' => 'Почасовая ставка',
+ 'Currency' => 'Валюта',
+ 'Effective date' => 'Дата вступления в силу',
+ 'Add new rate' => 'Добавить новый показатель',
+ 'Rate removed successfully.' => 'Показатель успешно удален.',
+ 'Unable to remove this rate.' => 'Не удается удалить этот показатель.',
+ 'Unable to save the hourly rate.' => 'Не удается сохранить почасовую ставку.',
+ 'Hourly rate created successfully.' => 'Почасовая ставка успешно создана.',
+ 'Start time' => 'Время начала',
+ 'End time' => 'Время завершения',
+ 'Comment' => 'Комментарий',
+ 'All day' => 'Весь день',
+ 'Day' => 'День',
+ 'Manage timetable' => 'Управление графиками',
+ 'Overtime timetable' => 'График сверхурочных',
+ 'Time off timetable' => 'Время в графике',
+ 'Timetable' => 'График',
+ 'Work timetable' => 'Work timetable',
+ 'Week timetable' => 'График на неделю',
+ 'Day timetable' => 'График на день',
+ 'From' => 'От кого',
+ 'To' => 'Кому',
+ 'Time slot created successfully.' => 'Временной интервал успешно создан.',
+ 'Unable to save this time slot.' => 'Невозможно сохранить этот временной интервал.',
+ 'Time slot removed successfully.' => 'Временной интервал успешно удален.',
+ 'Unable to remove this time slot.' => 'Не удается удалить этот временной интервал.',
+ 'Do you really want to remove this time slot?' => 'Вы действительно хотите удалить этот период времени?',
+ 'Remove time slot' => 'Удалить новый интервал времени',
+ 'Add new time slot' => 'Добавить новый интервал времени',
+ // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
+ 'Files' => 'Файлы',
+ 'Images' => 'Изображения',
+ 'Private project' => 'Приватный проект',
+ 'Amount' => 'Количество',
+ 'AUD - Australian Dollar' => 'AUD - Австралийский доллар',
+ 'Budget' => 'Бюджет',
+ 'Budget line' => 'Статья бюджета',
+ 'Budget line removed successfully.' => 'Бюджетная статья успешно удалена.',
+ 'Budget lines' => 'Статьи бюджета',
+ 'CAD - Canadian Dollar' => 'CAD - Канадский доллар',
+ 'CHF - Swiss Francs' => 'CHF - Швейцарский Франк',
+ 'Cost' => 'Стоимость',
+ 'Cost breakdown' => 'Детализация затрат',
+ 'Custom Stylesheet' => 'Пользовательский стиль',
+ 'download' => 'загрузить',
+ 'Do you really want to remove this budget line?' => 'Вы действительно хотите удалить эту статью бюджета?',
+ 'EUR - Euro' => 'EUR - Евро',
+ 'Expenses' => 'Расходы',
+ 'GBP - British Pound' => 'GBP - Британский фунт',
+ 'INR - Indian Rupee' => 'INR - Индийский рупий',
+ 'JPY - Japanese Yen' => 'JPY - Японскай йена',
+ 'New budget line' => 'Новая статья бюджета',
+ 'NZD - New Zealand Dollar' => 'NZD - Новозеландский доллар',
+ 'Remove a budget line' => 'Удалить строку в бюджете',
+ 'Remove budget line' => 'Удалить статью бюджета',
+ 'RSD - Serbian dinar' => 'RSD - Сербский динар',
+ 'The budget line have been created successfully.' => 'Статья бюджета успешно создана.',
+ 'Unable to create the budget line.' => 'Не удается создать эту статью бюджета.',
+ 'Unable to remove this budget line.' => 'Не удается удалить эту статью бюджета.',
+ 'USD - US Dollar' => 'USD - доллар США',
+ 'Remaining' => 'Прочее',
+ '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' => 'Исходная колонка',
+ 'Show subtask estimates (forecast of future work)' => 'Показать оценку подзадач (прогноз будущей работы)',
+ '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' => 'Add a new currency rate',
+ 'Currency rates are used to calculate project budget.' => 'Курсы валют используются для расчета бюджета проекта.',
+ 'Reference currency' => 'Справочник валют',
+ 'The currency rate have been added successfully.' => 'Курс валюты был успешно добавлен.',
+ 'Unable to add this currency rate.' => 'Невозможно добавить этот курс валюты.',
+ 'Send notifications to a Slack channel' => 'Отправлять уведомления в канал Slack',
+ 'Webhook URL' => 'Webhook URL',
+ 'Help on Slack integration' => 'Помощь по интеграции Slack',
+ '%s remove the assignee of the task %s' => '%s удалить назначенную задачу %s',
+ 'Send notifications to Hipchat' => 'Отправлять уведомления в Hipchat',
+ 'API URL' => 'API URL',
+ 'Room API ID or name' => 'API ID комнаты или имя',
+ 'Room notification token' => 'Ключь комнаты для уведомлений',
+ 'Help on Hipchat integration' => 'Помощь по интеграции Hipchat',
+ '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' => 'Двухфакторная авторизация',
+ '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' => '',
+ // 'uploaded on: %s' => '',
+ 'size: %s' => 'размер: %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.' => 'Сделайте скриншот и нажмите CTRL+V или ⌘+V для вложения',
+ 'Screenshot uploaded successfully.' => 'Скриншет успешно загружен',
+ 'SEK - Swedish Krona' => 'SEK - Шведская крона',
+ 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Идентификатор проекта - это опциональный буквенно-цифровой код использующийся для идентификации проекта',
+ 'Identifier' => 'Идентификатор',
+ 'Postmark (incoming emails)' => 'Postmark (входящие сообщения)',
+ 'Help on Postmark integration' => 'Справка о Postmark интеграции',
+ 'Mailgun (incoming emails)' => 'Mailgun (входящие сообщения)',
+ 'Help on Mailgun integration' => 'Справка о Mailgun интеграции',
+ 'Sendgrid (incoming emails)' => 'Sendgrid (входящие сообщения)',
+ 'Help on Sendgrid integration' => 'Справка о Sendgrid интеграции',
+ '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' => 'Этот 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' => 'Когда задача перемещается из первой колонки',
+ 'When task is moved to last column' => 'Когда задача перемещается в последнюю колонку',
+ 'Year(s)' => 'Год(а)',
+ 'Jabber (XMPP)' => 'Jabber (XMPP)',
+ 'Send notifications to Jabber' => 'Отправлять уведомления в Jabber',
+ 'XMPP server address' => 'Адрес Jabber сервера',
+ 'Jabber domain' => 'Домен Jabber',
+ 'Jabber nickname' => 'Имя пользователя Jabber',
+ 'Multi-user chat room' => 'Многопользовательский чат',
+ 'Help on Jabber integration' => 'Помощь по интеграции Jabber',
+ 'The server address must use this format: "tcp://hostname:5222"' => 'Адрес сервера должен быть в формате: tcp://hostname:5222',
+ '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 данные',
+ 'Preferences' => 'Предпочтения',
+ 'Security' => 'Безопастность',
+ 'Two factor authentication disabled' => 'Двухфакторная аутентификация отключена',
+ 'Two factor authentication enabled' => 'Включена двухфакторная аутентификация',
+ 'Unable to update this user.' => 'Не удается обновить этого пользователя.',
+ 'There is no user management for private projects.' => 'Там нет управления пользователя для частных проектов',
+);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
new file mode 100644
index 00000000..fea7f10e
--- /dev/null
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -0,0 +1,925 @@
+<?php
+
+return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
+ 'None' => 'None',
+ 'edit' => 'izmeni',
+ 'Edit' => 'Izmeni',
+ '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',
+ 'Save' => 'Snimi',
+ 'Login' => 'Prijava',
+ 'Official website:' => 'Zvanična strana:',
+ 'Unassigned' => 'Nedodeljen',
+ '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' => 'Korisnik',
+ 'Password' => 'Lozinka',
+ 'Default project' => 'Podrazumevani projekat',
+ 'Administrator' => 'Administrator',
+ 'Sign in' => 'Odjava',
+ 'Users' => 'Korisnik',
+ 'No user' => 'Ne',
+ 'Forbidden' => 'Zabranjeno',
+ 'Access Forbidden' => 'Zabranjen prostup',
+ 'Only administrators can access to this page.' => 'Samo administrator može videti ovu stranu.',
+ 'Edit user' => 'Izmeni korisnika',
+ 'Logout' => 'Odjava',
+ 'Bad username or password' => 'Loše korisničko ime ili lozinka',
+ 'users' => 'korisnici',
+ 'projects' => 'projekti',
+ 'Edit project' => 'Izmeni projekat',
+ 'Name' => 'Ime',
+ 'Activated' => 'Aktiviran',
+ 'Projects' => 'Projekti',
+ 'No project' => 'Bez projekta',
+ 'Project' => 'Projekat',
+ 'Status' => 'Status',
+ 'Tasks' => 'Zadatak',
+ 'Board' => 'Tabla',
+ 'Actions' => 'Akcje',
+ 'Inactive' => 'Neaktivan',
+ 'Active' => 'Aktivan',
+ 'Column %d' => 'Kolona %d',
+ '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' => 'Izmeni 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',
+ 'Boards' => 'Table',
+ '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',
+ 'Add Column' => 'Dodaj kolunu',
+ 'Project "%s"' => 'Projekt "%s"',
+ 'Nobody assigned' => 'Niko nije dodeljen',
+ 'Assigned to %s' => 'Dodeljen korisniku %s',
+ 'Remove a column' => 'Ukloni kolonu',
+ 'Remove a column from a board' => 'Ukloni kolonu sa table',
+ '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',
+ 'More information' => 'Još informacja',
+ 'Database size:' => 'Veličina baze :',
+ 'Download the database' => 'Preuzmi bazu',
+ 'Optimize the database' => 'Optimizuj bazu',
+ '(VACUUM command)' => '(komanda VACUUM)',
+ '(Gzip compressed Sqlite file)' => '(Sqlite baza spakovana Gzip-om)',
+ 'User settings' => 'Korisnička podešavanja',
+ 'My default project:' => 'Moj podrazumevani projekat:',
+ 'Close a task' => 'Zatvori zadatak',
+ 'Do you really want to close this task: "%s"?' => 'Da li zaista želiš da zatvoriš ovaj zadatak: "%s"?',
+ 'Edit a task' => 'Izmeni zadatak',
+ 'Column' => 'Kolona',
+ 'Color' => 'Boja',
+ 'Assignee' => 'Dodeli',
+ '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',
+ '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.',
+ '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' => 'Lozinka je obavezna',
+ 'This value must be an integer' => 'Mora biti ceo broj',
+ 'The username must be unique' => 'Korisničko ime mora biti jedinstveno',
+ 'The username must be alphanumeric' => 'Korisničko ime sme sadržati samo brojeve i slova',
+ 'The user id is required' => 'ID korisnika je obavezan',
+ 'Passwords don\'t match' => 'Lozinke se ne podudaraju',
+ 'The confirmation is required' => 'Potvrda je obavezna',
+ 'The column is required' => 'Kolona je obavezna',
+ 'The project is required' => 'Projekat je obavezan',
+ 'The color is required' => 'Boja je obavezna',
+ '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',
+ 'The language is required' => 'Jezik je obavezan',
+ 'There is no active project, the first step is to create a new project.' => 'Nema aktivnih projekata. Potrebno je prvo napraviti novi projekat.',
+ 'Settings saved successfully.' => 'Podešavanja uspešno snimljena.',
+ 'Unable to save your settings.' => 'Nemoguće snimanje podešavanja.',
+ 'Database optimization done.' => 'Optimizacija baze je završena.',
+ 'Your project have been created successfully.' => 'Projekat je uspešno napravljen.',
+ 'Unable to create your project.' => 'Nemoguće kreiranje projekta.',
+ 'Project updated successfully.' => 'Projekt je uspeš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 uspešno uklonjen.',
+ 'Project activated successfully.' => 'Projekt uspešno aktiviran.',
+ 'Unable to activate this project.' => 'Nemoguće aktiviranje projekta.',
+ 'Project disabled successfully.' => 'Projekat uspešno deaktiviran.',
+ 'Unable to disable this project.' => 'nemoguće deaktiviranje projekta.',
+ 'Unable to open this task.' => 'Nemoguće otvaranje zadatka.',
+ 'Task opened successfully.' => 'Zadatak uspešno otvoren.',
+ 'Unable to close this task.' => 'Nije moguće zatvaranje ovog zadatka.',
+ 'Task closed successfully.' => 'Zadatak uspešno zatvoren.',
+ 'Unable to update your task.' => 'Nije moguće ažuriranje zadatka.',
+ 'Task updated successfully.' => 'Zadatak uspešno ažuriran.',
+ 'Unable to create your task.' => 'Nije moguće kreiranje zadatka.',
+ 'Task created successfully.' => 'Zadatak uspešno kreiran.',
+ 'User created successfully.' => 'Korisnik uspešno kreiran',
+ 'Unable to create your user.' => 'Nije uspelo kreiranje korisnika.',
+ 'User updated successfully.' => 'Korisnik uspešno ažuriran.',
+ 'Unable to update your user.' => 'Nije moguće ažuriranje korisnika.',
+ 'User removed successfully.' => 'Korisnik uspešno uklonjen.',
+ 'Unable to remove this user.' => 'Nije moguće uklanjanje korisnika.',
+ 'Board updated successfully.' => 'Tabla uspešno ažurirana.',
+ 'Ready' => 'Spreman',
+ 'Backlog' => 'Log',
+ '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',
+ 'No task' => 'bez zadataka',
+ 'Completed tasks' => 'Zatvoreni zadaci',
+ 'List of projects' => 'Spisak projekata',
+ 'Completed tasks for "%s"' => 'zatvoreni zadaci za "%s"',
+ '%d closed tasks' => '%d zatvorenih zadataka',
+ 'No task for this project' => 'Nema dodeljenih zadataka ovom projektu',
+ 'Public link' => 'Javni link',
+ 'There is no column in your project!' => 'Nema dodeljenih kolona ovom projektu',
+ 'Change assignee' => 'Izmeni dodelu',
+ 'Change assignee for the task "%s"' => 'Izmeni dodelu za ovaj 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',
+ 'limit' => 'ograničenje',
+ 'Task limit' => 'Ograničenje zadatka',
+ 'Task count' => 'Broj zadataka',
+ 'This value must be greater than %d' => 'Vrednost mora biti veća od %d',
+ 'Edit project access list' => 'Izmeni prava pristupa projektu',
+ 'Edit users access' => 'Izmeni korisnička prava',
+ 'Allow this user' => 'Dozvoli ovog korisnika',
+ 'Only those users have access to this project:' => 'Samo ovi korisnici imaju pristup projektu:',
+ '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',
+ 'You are not allowed to access to this project.' => 'Nije ti dozvoljen pristup ovom projektu.',
+ 'Comments' => 'Komentari',
+ 'Post comment' => 'Dodaj komentar',
+ '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 uspešno ostavljen',
+ 'Unable to create your comment.' => 'Nemoguće kreiranje komentara',
+ 'The description is required' => 'Opis je obavezan',
+ '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',
+ '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"' => 'Akcje 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.' => 'Kad se događaj desi izvrši odgovarajuću akciju',
+ 'Next step' => 'Sledeć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',
+ 'Close the task' => 'Zatvori zadatak',
+ 'Assign the task to a specific user' => 'Dodeli 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' => 'Premesti zadatak u drugu kolonu',
+ 'Move a task to another position in the same column' => 'Promeni poziciju zadatka u istoj koloni',
+ 'Task modification' => 'Izman zadatka',
+ 'Task creation' => 'Kreiranje zadatka',
+ 'Open a closed task' => 'Otvori zatvoreni zadatak',
+ 'Closing a task' => 'Zatvaranja zadatka',
+ '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',
+ 'Update this comment' => 'Ažuriraj komentar',
+ 'Comment updated successfully.' => 'Komentar uspešno ažuriran.',
+ 'Unable to update your comment.' => 'Neuspešno ažuriranje komentara.',
+ 'Remove a comment' => 'Obriši komentar',
+ '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.',
+ 'Details' => 'Detalji',
+ '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',
+ 'Reset all tokens' => 'Resetuj tokene',
+ 'All tokens have been regenerated.' => 'Svi tokeni su ponovo generisani.',
+ 'Unknown' => 'Nepoznat',
+ 'Last logins' => 'Poslednja prijava',
+ 'Login date' => 'Datum prijave',
+ 'Authentication method' => 'Metod autentikacije',
+ 'IP address' => 'IP adresa',
+ 'User agent' => 'Browser',
+ 'Persistent connections' => 'Stalna konekcija',
+ 'No session.' => 'Bez sesjie',
+ 'Expiration date' => 'Ističe',
+ 'Remember Me' => 'Zapamti me',
+ 'Creation date' => 'Datum kreiranja',
+ 'Filter by user' => 'Po korisniku',
+ 'Filter by due date' => 'Po terminu',
+ 'Everybody' => 'Svi',
+ 'Open' => 'Otvoreni',
+ 'Closed' => 'Zatvoreni',
+ 'Search' => 'Traži',
+ 'Nothing found.' => 'Ništa nije pronađeno',
+ 'Search in the project "%s"' => 'Traži u prijektu "%s"',
+ 'Due date' => 'Termin',
+ 'Others formats accepted: %s and %s' => 'Ostali formati: %s i %s',
+ 'Description' => 'Opis',
+ '%d comments' => '%d Komentara',
+ '%d comment' => '%d Komentar',
+ 'Email address invalid' => 'Pogrešan e-mail',
+ 'Your Google Account is not linked anymore to your profile.' => 'Tvoj google nalog više nije povezan sa profilom',
+ 'Unable to unlink your Google Account.' => 'Neuspešno ukidanje veze od Google naloga',
+ 'Google authentication failed' => 'Neuspešna Google autentikacija',
+ 'Unable to link your Google Account.' => 'Neuspešno povezivanje sa Google nalogom',
+ 'Your Google Account is linked to your profile successfully.' => 'Vaš Google nalog je uspešno povezan sa vašim profilom',
+ '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 #%d' => 'Zadatak #%d',
+ 'Task removed successfully.' => 'Zadatak uspeš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 da obrišem zadatak "%s"?',
+ 'Assign automatically a color based on a category' => 'Automatski dodeli boju po kategoriji',
+ 'Assign automatically a category based on a color' => 'Automatski dodeli kategoriju po boji',
+ 'Task creation or modification' => 'Kreiranje ili izmena zadatka',
+ 'Category' => 'Kategorija',
+ 'Category:' => 'Kategorija:',
+ 'Categories' => 'Kategorije',
+ 'Category not found.' => 'Kategorija nije pronađena',
+ 'Your category have been created successfully.' => 'Uspešno kreirana kategorija.',
+ 'Unable to create your category.' => 'Nije moguće kreirati kategoriju.',
+ 'Your category have been updated successfully.' => 'Kategorija je uspešno izmenjena',
+ 'Unable to update your category.' => 'Nemoguće izmeniti kategoriju',
+ 'Remove a category' => 'Obriši kategoriju',
+ 'Category removed successfully.' => 'Kategorija uspešno uklonjena.',
+ 'Unable to remove this category.' => 'Nije moguće ukloniti kategoriju.',
+ 'Category modification for the project "%s"' => 'Izmena kategorije za projekat "%s"',
+ 'Category Name' => 'Naziv kategorije',
+ 'Categories for the project "%s"' => 'Kategorije u projektu',
+ 'Add a new category' => 'Dodaj novu kategoriju',
+ 'Do you really want to remove this category: "%s"?' => 'Da li zaista želiš da ukloniš kategoriju: "%s"?',
+ 'Filter by category' => 'Po kategoriji',
+ '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.' => 'Uspešno uklonjen fajl.',
+ 'Attach a document' => 'Prikači dokument',
+ 'Do you really want to remove this file: "%s"?' => 'Da li da uklonim fajl: "%s"?',
+ 'open' => 'otvori',
+ 'Attachments' => 'Prilozi',
+ 'Edit the task' => 'Izmena Zadatka',
+ 'Edit the description' => 'Izmena opisa',
+ 'Add a comment' => 'Dodaj komentar',
+ 'Edit a comment' => 'Izmeni komentar',
+ 'Summary' => 'Pregled',
+ 'Time tracking' => 'Praćenje vremena',
+ 'Estimate:' => 'Procena:',
+ 'Spent:' => 'Potrošeno:',
+ 'Do you really want to remove this sub-task?' => 'Da li da uklonim pod-zdadatak?',
+ 'Remaining:' => 'Preostalo:',
+ 'hours' => 'sati',
+ 'spent' => 'potrošeno',
+ 'estimated' => 'procenjeno',
+ 'Sub-Tasks' => 'Pod-zadaci',
+ 'Add a sub-task' => 'Dodaj pod-zadatak',
+ 'Original estimate' => 'Originalna procena',
+ 'Create another sub-task' => 'Dodaj novi pod-zadatak',
+ 'Time spent' => 'Utrošeno vreme',
+ 'Edit a sub-task' => 'Izmeni pod-zadatak',
+ 'Remove a sub-task' => 'Ukloni pod-zadatak',
+ 'The time must be a numeric value' => 'Vreme mora biti broj',
+ 'Todo' => 'Za rad',
+ 'In progress' => 'U radu',
+ 'Sub-task removed successfully.' => 'Pod-zadatak uspešno uklonjen.',
+ 'Unable to remove this sub-task.' => 'Nie można usunąć tego pod-zadania.',
+ 'Sub-task updated successfully.' => 'Pod-zadatak zaktualizowane pomyślnie.',
+ 'Unable to update your sub-task.' => 'Nie można zaktalizować tego pod-zadania.',
+ 'Unable to create your sub-task.' => 'Nie można utworzyć tego pod-zadania.',
+ 'Sub-task added successfully.' => 'Pod-zadatak utworzone pomyślnie',
+ 'Maximum size: ' => 'Maksimalna veličina: ',
+ 'Unable to upload the file.' => 'Nije moguće snimiti fajl.',
+ 'Display another project' => 'Prikaži drugi projekat',
+ 'Your GitHub account was successfully linked to your profile.' => 'Konto Github podłączone pomyślnie.',
+ 'Unable to link your GitHub Account.' => 'Nie można połączyć z kontem Github.',
+ 'GitHub authentication failed' => 'Autentykacja Github nieudana',
+ 'Your GitHub account is no longer linked to your profile.' => 'Konto Github nie jest już podłączone do twojego profilu.',
+ 'Unable to unlink your GitHub Account.' => 'Nie można odłączyć konta Github.',
+ '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',
+ 'End Date' => 'Krajni datum',
+ 'Execute' => 'Izvrši',
+ 'Task Id' => 'Identifikator Zadatka',
+ 'Creator' => 'Autor',
+ 'Modification date' => 'Datum izmene',
+ 'Completion date' => 'Datum kompletiranja',
+ 'Clone' => 'Iskopiraj',
+ 'Clone Project' => 'Iskopiraj projekat',
+ 'Project cloned successfully.' => 'Projekat uspešno iskopiran.',
+ 'Unable to clone this project.' => 'Nije moguće iskopirati projekat.',
+ 'Email notifications' => 'Obaveštenje e-mailom',
+ 'Enable email notifications' => 'Omogući obaveš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 izmenjen',
+ 'Title:' => 'Naslov:',
+ // 'Status:' => '',
+ 'Assignee:' => 'Dodeli:',
+ 'Time tracking:' => 'Praćenje vremena: ',
+ 'New sub-task' => 'Novi Pod-zadatak',
+ 'New attachment added "%s"' => 'Novi prilog ubačen "%s"',
+ 'Comment updated' => 'Komentar izmenjen',
+ 'New comment posted by %s' => 'Novi komentar ostavio %s',
+ 'List of due tasks for the project "%s"' => 'Spisak dospelih zadataka za projekat "%s"',
+ // 'New attachment' => '',
+ // 'New comment' => '',
+ // 'New subtask' => '',
+ // 'Subtask updated' => '',
+ // 'Task updated' => '',
+ 'Task closed' => 'Zadatak je zatvoren',
+ 'Task opened' => 'Zadatak je otvoren',
+ '[%s][Due tasks]' => '[%s][Dospeli zadaci]',
+ '[Kanboard] Notification' => '[Kanboard] Obaveštenja',
+ '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',
+ 'Category management' => 'Uređivanje kategorija',
+ 'User management' => 'Uređivanje korisnika',
+ 'Active tasks' => 'Aktivni zadaci',
+ 'Disable public access' => 'Zabrani javni pristup',
+ 'Enable public access' => 'Dozvoli javni pristup',
+ 'Active projects' => 'Aktivni projekti',
+ 'Inactive projects' => 'Neaktivni projekti',
+ '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 duplicate this project: "%s"?' => 'Da li da napravim kopiju ovog projekta: "%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' => 'Premesti zadatak u drugi projekat',
+ 'Move to another project' => 'Premesti u drugi projekat',
+ 'Do you really want to duplicate this task?' => 'Da li da napravim kopiju ovog projekta: "%s"?',
+ 'Duplicate a task' => 'Kopiraj zadatak',
+ 'External accounts' => 'Spoljni nalozi',
+ 'Account type' => 'Tip naloga',
+ 'Local' => 'Lokalno',
+ 'Remote' => 'Udaljno',
+ 'Enabled' => 'Omogući',
+ 'Disabled' => 'Onemogući',
+ 'Google account linked' => 'Połączone konto Google',
+ 'Github account linked' => 'Połączone konto Github',
+ 'Username:' => 'Korisničko ime:',
+ 'Name:' => 'Ime i Prezime',
+ 'Email:' => 'Email: ',
+ 'Default project:' => 'Osnovni projekat:',
+ '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.',
+ 'Change category for the task "%s"' => 'Izmeni kategoriju zadatka "%s"',
+ 'Change category' => 'Izmeni kategoriju',
+ '%s updated the task %s' => '%s izmeni zadatak %s',
+ '%s opened the task %s' => '%s aktivni zadaci %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s premešten zadatak %s na poziciju #%d u koloni "%s"',
+ '%s moved the task %s to the column "%s"' => '%s premešten 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 kreiran pod-zadatak zadatka %s',
+ '%s updated a subtask for the task %s' => '%s izmenjen pod-zadatak zadatka %s',
+ 'Assigned to %s with an estimate of %s/%sh' => 'Dodeljen korisniku %s uz procenu vremena %s/%sh',
+ 'Not assigned, estimate of %sh' => 'Ne dodeljen, procenjeno vreme %sh',
+ '%s updated a comment on the task %s' => '%s izmenjen komentar zadatka %s',
+ '%s commented the task %s' => '%s komentarisao zadatak %s',
+ '%s\'s activity' => 'Aktivnosti %s',
+ 'No activity.' => 'Bez aktivnosti.',
+ 'RSS feed' => 'RSS kanal',
+ '%s updated a comment on the task #%d' => '%s izmenjen komentar zadatka #%d',
+ '%s commented on the task #%d' => '%s komentarisao zadatak #%d',
+ '%s updated a subtask for the task #%d' => '%s izmenjen pod-zadatak zadatka #%d',
+ '%s created a subtask for the task #%d' => '%s kreirao pod-zadatak zadatka #%d',
+ '%s updated the task #%d' => '%s izmenjen 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 premestio zadatak #%d u kolonu "%s"',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s premestio zadatak #%d na pozycję %d w kolmnie "%s"',
+ 'Activity' => 'Aktivnosti',
+ 'Default values are "%s"' => 'Osnovne vrednosti su: "%s"',
+ 'Default columns for new projects (Comma-separated)' => 'Osnovne kolone za novi projekat (Odvojeni zarezom)',
+ 'Task assignee change' => 'Zmień osobę odpowiedzialną',
+ '%s change the assignee of the task #%d to %s' => '%s zamena dodele za zadatak #%d na %s',
+ '%s changed the assignee of the task %s to %s' => '%s zamena dodele za zadatak %s na %s',
+ // 'Column Change' => '',
+ // 'Position Change' => '',
+ // 'Assignee Change' => '',
+ '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',
+ 'Database driver:' => 'Database driver:',
+ 'Board settings' => 'Podešavanje table',
+ 'URL and token' => 'URL i token',
+ // 'Webhook settings' => '',
+ 'URL for task creation:' => 'URL za kreiranje zadataka',
+ 'Reset token' => 'Resetuj token',
+ // 'API endpoint:' => '',
+ 'Refresh interval for private board' => 'Interval osvežavanja privatnih tabli',
+ 'Refresh interval for public board' => 'Interval osvežavanja javnih tabli',
+ 'Task highlight period' => 'Task highlight period',
+ // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
+ // 'Frequency in second (60 seconds by default)' => '',
+ // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
+ 'Application URL' => '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"',
+ 'New private project' => 'Novi privatni projekat',
+ 'This project is private' => 'Ovaj projekat je privatan',
+ 'Type here to create a new sub-task' => 'Kucaj ovde za kreiranje novog pod-zadatka',
+ 'Add' => 'Dodaj',
+ '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',
+ 'My tasks' => 'Moji zadaci',
+ 'Activity stream' => 'Spisak aktinosti',
+ 'Dashboard' => 'Panel',
+ 'Confirmation' => 'Potvrda',
+ 'Allow everybody to access to this project' => 'Dozvoli svima pristup projektu',
+ 'Everybody have access to this project.' => 'Svima je dozvoljen pristup.',
+ // 'Webhooks' => '',
+ // 'API' => '',
+ 'Integration' => 'Integracja',
+ // 'Github webhooks' => '',
+ // 'Help on Github webhooks' => '',
+ // 'Create a comment from an external provider' => '',
+ // 'Github issue comment created' => '',
+ 'Configure' => 'Podesi',
+ 'Project management' => 'Uređivanje projekata',
+ 'My projects' => 'Moji projekti',
+ 'Columns' => 'Kolone',
+ 'Task' => 'Zadaci',
+ 'Your are not member of any project.' => 'Nisi član ni jednog projekta',
+ 'Percentage' => 'Procenat',
+ 'Number of tasks' => 'Broj zadataka',
+ 'Task distribution' => 'Podela 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' => 'Kopiraj projekat',
+ 'Column removed successfully.' => 'Kolumna usunięta pomyslnie.',
+ 'Edit Project' => 'Izmeni projekat',
+ // '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ą',
+ '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-zadatak jest wymagane',
+ 'The subtask id must be an integer' => 'ID pod-zadania musi być liczbą całkowitą',
+ 'The task id is required' => 'ID zadania jest wymagane',
+ 'The task id must be an integer' => 'ID zadatka mora biti broj',
+ 'The user id must be an integer' => 'ID korisnika mora biti broj',
+ 'This value is required' => 'Vrednost je obavezna',
+ 'This value must be numeric' => 'Vrednost 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 zbirnig pregleda po danima za "%s"',
+ 'Exports' => 'Izvoz',
+ // 'This export contains the number of tasks per column grouped per day.' => '',
+ 'Nothing to preview...' => 'Ništa za prikazivanje...',
+ 'Preview' => 'Pregled',
+ 'Write' => 'Piši',
+ 'Active swimlanes' => 'Aktivni razdelnik',
+ 'Add a new swimlane' => 'Dodaj razdelnik',
+ 'Change default swimlane' => 'Zameni osnovni razdelnik',
+ '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.',
+ 'Swimlane removed successfully.' => 'Razdelnik uspešno uklonjen.',
+ '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ń',
+ 'Subtasks exportation for "%s"' => 'Izvoz pod-zadań dla "%s"',
+ 'Task Title' => 'Naslov zadatka',
+ 'Untitled' => 'Bez naslova',
+ 'Application default' => 'Postavke aplikacje',
+ 'Language:' => 'Jezik:',
+ 'Timezone:' => 'Vremenska zona:',
+ 'All columns' => 'Sve kolone',
+ 'Calendar for "%s"' => 'Kalendar za "%s"',
+ 'Filter by column' => 'Po koloni',
+ 'Filter by status' => 'Po statusu',
+ 'Calendar' => 'Kalendar',
+ 'Next' => 'Sledeći',
+ // '#%d' => '',
+ 'Filter by color' => 'Po boji',
+ 'Filter by swimlane' => 'Po razdelniku',
+ 'All swimlanes' => 'Svi razdelniki',
+ 'All colors' => 'Sve boje',
+ 'All status' => 'Svi statusi',
+ 'Add a comment logging moving the task between columns' => 'Dodaj logovanje premeštanja zadataka po kolonama',
+ 'Moved to column %s' => 'Premešten u kolonu %s',
+ // 'Change description' => '',
+ 'User dashboard' => 'Korisnički panel',
+ // 'Allow only one subtask in progress at the same time for a user' => '',
+ // 'Edit column "%s"' => '',
+ // 'Enable time tracking for subtasks' => '',
+ // 'Select the new status of the subtask: "%s"' => '',
+ // 'Subtask timesheet' => '',
+ 'There is nothing to show.' => 'Nema podataka',
+ 'Time Tracking' => 'Praćenje vremena',
+ // 'You already have one subtask in progress' => '',
+ 'Which parts of the project do you want to duplicate?' => 'Koje delove projekta želite da kopirate',
+ // 'Change dashboard view' => '',
+ // 'Show/hide activities' => '',
+ // 'Show/hide projects' => '',
+ // 'Show/hide subtasks' => '',
+ // 'Show/hide tasks' => '',
+ // 'Disable login form' => '',
+ // 'Show/hide calendar' => '',
+ // 'User calendar' => '',
+ // 'Bitbucket commit received' => '',
+ // 'Bitbucket webhooks' => '',
+ // 'Help on Bitbucket webhooks' => '',
+ // 'Start' => '',
+ // 'End' => '',
+ // 'Task age in days' => '',
+ // 'Days in this column' => '',
+ // '%dd' => '',
+ 'Add a link' => 'Dodaj link',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
+ // 'Filter recently updated' => '',
+ // 'since %B %e, %Y at %k:%M %p' => '',
+ // 'More filters' => '',
+ // 'Compact view' => '',
+ // 'Horizontal scrolling' => '',
+ // 'Compact/wide view' => '',
+ // 'No results match:' => '',
+ // 'Remove hourly rate' => '',
+ // 'Do you really want to remove this hourly rate?' => '',
+ // 'Hourly rates' => '',
+ // 'Hourly rate' => '',
+ // 'Currency' => '',
+ // 'Effective date' => '',
+ // 'Add new rate' => '',
+ // 'Rate removed successfully.' => '',
+ // 'Unable to remove this rate.' => '',
+ // 'Unable to save the hourly rate.' => '',
+ // 'Hourly rate created successfully.' => '',
+ // 'Start time' => '',
+ // 'End time' => '',
+ // 'Comment' => '',
+ // 'All day' => '',
+ // 'Day' => '',
+ // 'Manage timetable' => '',
+ // 'Overtime timetable' => '',
+ // 'Time off timetable' => '',
+ // 'Timetable' => '',
+ // 'Work timetable' => '',
+ // 'Week timetable' => '',
+ // 'Day timetable' => '',
+ // 'From' => '',
+ // 'To' => '',
+ // 'Time slot created successfully.' => '',
+ // 'Unable to save this time slot.' => '',
+ // 'Time slot removed successfully.' => '',
+ // 'Unable to remove this time slot.' => '',
+ // 'Do you really want to remove this time slot?' => '',
+ // 'Remove time slot' => '',
+ // 'Add new time slot' => '',
+ // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
+ // 'Files' => '',
+ // 'Images' => '',
+ // 'Private project' => '',
+ // 'Amount' => '',
+ // 'AUD - Australian Dollar' => '',
+ // 'Budget' => '',
+ // 'Budget line' => '',
+ // 'Budget line removed successfully.' => '',
+ // 'Budget lines' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ // 'Cost' => '',
+ // 'Cost breakdown' => '',
+ // 'Custom Stylesheet' => '',
+ // 'download' => '',
+ // 'Do you really want to remove this budget line?' => '',
+ // 'EUR - Euro' => '',
+ // 'Expenses' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'New budget line' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'Remove a budget line' => '',
+ // 'Remove budget line' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'The budget line have been created successfully.' => '',
+ // 'Unable to create the budget line.' => '',
+ // 'Unable to remove this budget line.' => '',
+ // 'USD - US Dollar' => '',
+ // 'Remaining' => '',
+ // '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' => '',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ // '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' => '',
+ // 'Currency rates are used to calculate project budget.' => '',
+ // 'Reference currency' => '',
+ // 'The currency rate have been added successfully.' => '',
+ // 'Unable to add this currency rate.' => '',
+ // 'Send notifications to a Slack channel' => '',
+ // 'Webhook URL' => '',
+ // 'Help on Slack integration' => '',
+ // '%s remove the assignee of the task %s' => '',
+ // 'Send notifications to Hipchat' => '',
+ // 'API URL' => '',
+ // 'Room API ID or name' => '',
+ // 'Room notification token' => '',
+ // 'Help on Hipchat integration' => '',
+ // '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.' => '',
+ // 'SEK - Swedish Krona' => '',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
+);
diff --git a/app/Locales/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index 71e03090..b4056321 100644
--- a/app/Locales/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -1,6 +1,8 @@
<?php
return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => '.',
'None' => 'Ingen',
'edit' => 'redigera',
'Edit' => 'Redigera',
@@ -104,7 +106,7 @@ 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 %d %B %Y kl %H:%M',
+ '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',
@@ -166,8 +168,8 @@ 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 %d %B %Y kl %H:%M',
- '%B %e, %Y at %k:%M %p' => '%d %B %Y kl %H:%M',
+ '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',
@@ -182,18 +184,19 @@ return array(
'Change assignee' => 'Ändra uppdragsinnehavare',
'Change assignee for the task "%s"' => 'Ändra uppdragsinnehavare för uppgiften "%s"',
'Timezone' => 'Tidszon',
- 'Sorry, I didn\'t found this information in my database!' => 'Informationen kunde inte hittas i databasen.',
+ 'Sorry, I didn\'t find this information in my database!' => 'Informationen kunde inte hittas i databasen.',
'Page not found' => 'Sidan hittas inte',
- 'Complexity' => 'Ungefärligt antal timmar',
+ 'Complexity' => 'Komplexitet',
'limit' => 'max',
'Task limit' => 'Uppgiftsbegränsning',
+ 'Task count' => 'Antal uppgifter',
'This value must be greater than %d' => 'Värdet måste vara större än %d',
'Edit project access list' => 'Ändra projektåtkomst lista',
'Edit users access' => 'Användaråtkomst',
'Allow this user' => 'Tillåt användare',
'Only those users have access to this project:' => 'Bara de användarna har tillgång till detta projekt.',
'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',
+ '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.',
@@ -210,8 +213,9 @@ 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 %B %e, %Y',
- '%B %e, %Y' => '%d %B %Y',
+ '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.',
@@ -375,7 +379,7 @@ return array(
'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 %B %e, %Y kl %k:%M %p',
+ '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',
@@ -385,8 +389,6 @@ return array(
'Creator' => 'Skapare',
'Modification date' => 'Ändringsdatum',
'Completion date' => 'Slutfört datum',
- 'Webhook URL for task creation' => 'Webhook URL för att skapa uppgift',
- 'Webhook URL for task modification' => 'Webhook URL för att ändra uppgift',
'Clone' => 'Klona',
'Clone Project' => 'Klona projekt',
'Project cloned successfully.' => 'Projektet har klonats.',
@@ -406,15 +408,13 @@ return array(
'Comment updated' => 'Kommentaren har uppdaterats',
'New comment posted by %s' => 'Ny kommentar postad av %s',
'List of due tasks for the project "%s"' => 'Lista med uppgifter för projektet "%s"',
- '[%s][New attachment] %s (#%d)' => '[%s][Ny bifogning] %s (#%d)',
- '[%s][New comment] %s (#%d)' => '[%s][Ny kommentar] %s (#%d)',
- '[%s][Comment updated] %s (#%d)' => '[%s][Uppdaterad kommentar] %s (#%d)',
- '[%s][New subtask] %s (#%d)' => '[%s][Ny deluppgift] %s (#%d)',
- '[%s][Subtask updated] %s (#%d)' => '[%s][Deluppgiften uppdaterad] %s (#%d)',
- '[%s][New task] %s (#%d)' => '[%s][Ny uppgift] %s (#%d)',
- '[%s][Task updated] %s (#%d)' => '[%s][Uppgiften uppdaterad] %s (#%d)',
- '[%s][Task closed] %s (#%d)' => '[%s][Uppgiften stängd] %s (#%d)',
- '[%s][Task opened] %s (#%d)' => '[%s][Uppgiften öppnad] %s (#%d)',
+ 'New attachment' => 'Ny bifogning',
+ 'New comment' => 'Ny kommentar',
+ 'New subtask' => 'Ny deluppgift',
+ 'Subtask updated' => 'Deluppgiften har uppdaterats',
+ 'Task updated' => 'Uppgiften har uppdaterats',
+ 'Task closed' => 'Uppgiften har stängts',
+ 'Task opened' => 'Uppgiften har öppnats',
'[%s][Due tasks]' => '[%s][Förfallen uppgift]',
'[Kanboard] Notification' => '[Kanboard] Notis',
'I want to receive notifications only for those projects:' => 'Jag vill endast få notiser för dessa projekt:',
@@ -467,18 +467,18 @@ return array(
'Unable to change the password.' => 'Kunde inte byta lösenord.',
'Change category for the task "%s"' => 'Byt kategori för uppgiften "%s"',
'Change category' => 'Byt kategori',
- '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s uppdaterade uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s öppna uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s flyttade uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> till positionen #%d i kolumnen "%s"',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s flyttade uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> till kolumnen "%s"',
- '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s skapade uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s stängde uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s skapade en deluppgift för uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s uppdaterade en deluppgift för uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
+ '%s updated the task %s' => '%s uppdaterade uppgiften %s',
+ '%s opened the task %s' => '%s öppna uppgiften %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s flyttade uppgiften %s till positionen #%d i kolumnen "%s"',
+ '%s moved the task %s to the column "%s"' => '%s flyttade uppgiften %s till kolumnen "%s"',
+ '%s created the task %s' => '%s skapade uppgiften %s',
+ '%s closed the task %s' => '%s stängde uppgiften %s',
+ '%s created a subtask for the task %s' => '%s skapade en deluppgift för uppgiften %s',
+ '%s updated a subtask for the task %s' => '%s uppdaterade en deluppgift för uppgiften %s',
'Assigned to %s with an estimate of %s/%sh' => 'Tilldelades %s med en uppskattning på %s/%sh',
'Not assigned, estimate of %sh' => 'Inte tilldelade, uppskattat %sh',
- '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s uppdaterade en kommentar till uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s kommenterade uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
+ '%s updated a comment on the task %s' => '%s uppdaterade en kommentar till uppgiften %s',
+ '%s commented the task %s' => '%s kommenterade uppgiften %s',
'%s\'s activity' => '%s\'s aktivitet',
'No activity.' => 'Ingen aktivitet.',
'RSS feed' => 'RSS flöde',
@@ -497,10 +497,10 @@ return array(
'Default columns for new projects (Comma-separated)' => 'Standardkolumner för nya projekt (kommaseparerade)',
'Task assignee change' => 'Ändra tilldelning av uppgiften',
'%s change the assignee of the task #%d to %s' => '%s byt tilldelning av uppgiften #%d till %s',
- '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '%s byt tilldelning av uppgiften <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> till %s',
- '[%s][Column Change] %s (#%d)' => '[%s][Byt kolumn] %s (#%d)',
- '[%s][Position Change] %s (#%d)' => '[%s][Byt position] %s (#%d)',
- '[%s][Assignee Change] %s (#%d)' => '[%s][Byt tilldelning] %s (#%d)',
+ '%s changed the assignee of the task %s to %s' => '%s byt tilldelning av uppgiften %s till %s',
+ 'Column Change' => 'Ändring av kolumn',
+ 'Position Change' => 'Ändring av position',
+ 'Assignee Change' => 'Ändring av tilldelning',
'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',
@@ -541,14 +541,385 @@ return array(
'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 den %B %e, %Y',
+ '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.',
- 'My tasks' => 'Mina uppgifte',
+ 'My tasks' => 'Mina uppgifter',
'Activity stream' => 'Aktivitetsström',
'Dashboard' => 'Instrumentpanel',
'Confirmation' => 'Bekräftelse',
- // 'Allow everybody to access to this project' => '',
- // 'Everybody have access to this project.' => '',
+ 'Allow everybody to access to this project' => 'Ge alla tillgång till projektet',
+ 'Everybody have access to this project.' => 'Alla har tillgång till projektet',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Integration' => 'Integration',
+ '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',
+ 'Configure' => 'Konfigurera',
+ 'Project management' => 'Projekthantering',
+ 'My projects' => 'Mina projekt',
+ 'Columns' => 'Kolumner',
+ 'Task' => 'Uppgift',
+ 'Your are not member of any project.' => 'Du är inte medlem i något projekt',
+ 'Percentage' => 'Procent',
+ 'Number of tasks' => 'Antal uppgifter',
+ 'Task distribution' => 'Uppgiftsfördelning',
+ 'Reportings' => 'Rapportering',
+ 'Task repartition for "%s"' => 'Uppgiftsdeltagande för "%s"',
+ 'Analytics' => 'Analyser',
+ 'Subtask' => 'Deluppgift',
+ 'My subtasks' => 'Mina deluppgifter',
+ 'User repartition' => 'Användardeltagande',
+ 'User repartition for "%s"' => 'Användardeltagande för "%s"',
+ 'Clone this project' => 'Klona projektet',
+ 'Column removed successfully.' => 'Kolumnen togs bort',
+ 'Edit Project' => 'Ändra Projekt',
+ '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',
+ 'The project id must be an integer' => 'Projekt-ID måste vara ett heltal',
+ 'The status must be an integer' => 'Status måste vara ett heltal',
+ 'The subtask id is required' => 'Deluppgifts-ID behövs',
+ 'The subtask id must be an integer' => 'Deluppgifts-ID måste vara ett heltal',
+ 'The task id is required' => 'Uppgifts-ID behövs',
+ 'The task id must be an integer' => 'Uppgifts-ID måste vara ett heltal',
+ 'The user id must be an integer' => 'Användar-ID måste vara ett heltal',
+ 'This value is required' => 'Värdet behövs',
+ 'This value must be numeric' => 'Värdet måste vara numeriskt',
+ 'Unable to create this task.' => 'Kunde inte skapa uppgiften.',
+ 'Cumulative flow diagram' => 'Diagram med kumulativt flöde',
+ 'Cumulative flow diagram for "%s"' => 'Diagram med kumulativt flöde för "%s"',
+ 'Daily project summary' => 'Daglig projektsummering',
+ 'Daily project summary export' => 'Export av daglig projektsummering',
+ 'Daily project summary export for "%s"' => 'Export av daglig projektsummering för "%s"',
+ 'Exports' => 'Exporter',
+ 'This export contains the number of tasks per column grouped per day.' => 'Denna export innehåller antalet uppgifter per kolumn grupperade per dag.',
+ 'Nothing to preview...' => 'Inget att förhandsgrandska...',
+ 'Preview' => 'Förhandsgranska',
+ 'Write' => 'Skriva',
+ 'Active swimlanes' => 'Aktiva swimlanes',
+ 'Add a new swimlane' => 'Lägg till en nytt swimlane',
+ 'Change default swimlane' => 'Ändra standard swimlane',
+ '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',
+ 'Swimlane removed successfully.' => 'Swimlane togs bort',
+ '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',
+ 'Subtasks exportation for "%s"' => 'Export av deluppgifter för "%s"',
+ 'Task Title' => 'Uppgiftstitel',
+ 'Untitled' => 'Titel saknas',
+ 'Application default' => 'Applikationsstandard',
+ 'Language:' => 'Språk',
+ 'Timezone:' => 'Tidszon',
+ 'All columns' => 'Alla kolumner',
+ 'Calendar for "%s"' => 'Kalender för "%s"',
+ 'Filter by column' => 'Filtrera på kolumn',
+ 'Filter by status' => 'Filtrera på status',
+ 'Calendar' => 'Kalender',
+ 'Next' => 'Nästa',
+ '#%d' => '#%d',
+ 'Filter by color' => 'Filtrera på färg',
+ 'Filter by swimlane' => 'Filtrera på swimlane',
+ 'All swimlanes' => 'Alla swimlanes',
+ 'All colors' => 'Alla färger',
+ 'All status' => 'Alla status',
+ 'Add a comment logging moving the task between columns' => 'Lägg till en kommentar för att logga förflyttning av en uppgift mellan kolumner',
+ 'Moved to column %s' => 'Flyttad till kolumn %s',
+ 'Change description' => 'Ändra beskrivning',
+ 'User dashboard' => 'Användardashboard',
+ 'Allow only one subtask in progress at the same time for a user' => 'Tillåt endast en deluppgift igång samtidigt för en användare',
+ 'Edit column "%s"' => 'Ändra kolumn "%s"',
+ 'Enable time tracking for subtasks' => 'Aktivera tidsbevakning för deluppgifter',
+ 'Select the new status of the subtask: "%s"' => 'Välj ny status för deluppgiften: "%s"',
+ 'Subtask timesheet' => 'Tidrapport för deluppgiften',
+ 'There is nothing to show.' => 'Det finns inget att visa',
+ 'Time Tracking' => 'Tidsbevakning',
+ '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?',
+ 'Change dashboard view' => 'Ändra dashboard vy',
+ 'Show/hide activities' => 'Visa/dölj aktiviteter',
+ 'Show/hide projects' => 'Visa/dölj projekt',
+ 'Show/hide subtasks' => 'Visa/dölj deluppgifter',
+ 'Show/hide tasks' => 'Visa/dölj uppgifter',
+ 'Disable login form' => 'Inaktivera loginformuläret',
+ 'Show/hide calendar' => 'Visa/dölj kalender',
+ 'User calendar' => 'Användarkalender',
+ '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',
+ 'Days in this column' => 'Dagar i denna kolumn',
+ '%dd' => '%dd',
+ 'Add a link' => 'Lägg till länk',
+ 'Add a new link' => 'Lägg till ny länk',
+ 'Do you really want to remove this link: "%s"?' => 'Vill du verkligen ta bort länken: "%s"?',
+ 'Do you really want to remove this link with task #%d?' => 'Vill du verkligen ta bort länken till uppgiften #%d?',
+ 'Field required' => 'Fältet krävs',
+ 'Link added successfully.' => 'Länken har lagts till',
+ 'Link updated successfully.' => 'Länken har uppdaterats',
+ 'Link removed successfully.' => 'Länken har tagits bort',
+ 'Link labels' => 'Länketiketter',
+ 'Link modification' => 'Länkändring',
+ 'Links' => 'Länkar',
+ 'Link settings' => 'Länkinställningar',
+ 'Opposite label' => 'Motpartslänk',
+ 'Remove a link' => 'Ta bort en länk',
+ 'Task\'s links' => 'Uppgiftslänkar',
+ 'The labels must be different' => 'Etiketterna måste vara olika',
+ 'There is no link.' => 'Det finns ingen länk',
+ 'This label must be unique' => 'Länken måste vara unik',
+ 'Unable to create your link.' => 'Kunde inte skapa din länk',
+ 'Unable to update your link.' => 'Kunde inte uppdatera din länk',
+ 'Unable to remove this link.' => 'Kunde inte ta bort din länk',
+ 'relates to' => 'relaterar till',
+ 'blocks' => 'blockerar',
+ 'is blocked by' => 'blockeras av',
+ 'duplicates' => 'dupplicerar',
+ 'is duplicated by' => 'är duplicerad av',
+ 'is a child of' => 'är underliggande till',
+ 'is a parent of' => 'är överliggande till',
+ 'targets milestone' => 'milstolpemål',
+ 'is a milestone of' => 'är en milstolpe för',
+ 'fixes' => 'åtgärdar',
+ 'is fixed by' => 'åtgärdas av',
+ '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',
+ 'Close dialog box' => 'Stäng dialogruta',
+ 'Submit a form' => 'Sänd formulär',
+ 'Board view' => 'Tavelvy',
+ 'Keyboard shortcuts' => 'Tangentbordsgenvägar',
+ 'Open board switcher' => 'Växling av öppen tavla',
+ 'Application' => 'Applikation',
+ 'Filter recently updated' => 'Filter som uppdaterats nyligen',
+ 'since %B %e, %Y at %k:%M %p' => 'sedan %B %e, %Y at %k:%M %p',
+ 'More filters' => 'Fler filter',
+ 'Compact view' => 'Kompakt vy',
+ 'Horizontal scrolling' => 'Horisontell scroll',
+ 'Compact/wide view' => 'Kompakt/bred vy',
+ 'No results match:' => 'Inga matchande resultat',
+ 'Remove hourly rate' => 'Ta bort timtaxa',
+ 'Do you really want to remove this hourly rate?' => 'Vill du verkligen ta bort denna timtaxa?',
+ 'Hourly rates' => 'Timtaxor',
+ 'Hourly rate' => 'Timtaxa',
+ 'Currency' => 'Valuta',
+ 'Effective date' => 'Giltighetsdatum',
+ 'Add new rate' => 'Lägg till ny taxa',
+ 'Rate removed successfully.' => 'Taxan togs bort.',
+ 'Unable to remove this rate.' => 'Kunde inte ta bort taxan.',
+ 'Unable to save the hourly rate.' => 'Kunde inte spara timtaxan.',
+ 'Hourly rate created successfully.' => 'Timtaxan skapades.',
+ 'Start time' => 'Starttid',
+ 'End time' => 'Sluttid',
+ 'Comment' => 'Kommentar',
+ 'All day' => 'Hela dagen',
+ 'Day' => 'Dag',
+ 'Manage timetable' => 'Hantera timplan',
+ 'Overtime timetable' => 'Övertidstimplan',
+ 'Time off timetable' => 'Ledighetstimplan',
+ 'Timetable' => 'Timplan',
+ 'Work timetable' => 'Arbetstimplan',
+ 'Week timetable' => 'Veckotidplan',
+ 'Day timetable' => 'Dagstimplan',
+ 'From' => 'Från',
+ 'To' => 'Till',
+ 'Time slot created successfully.' => 'Tidslucka skapad.',
+ 'Unable to save this time slot.' => 'Kunde inte spara tidsluckan.',
+ 'Time slot removed successfully.' => 'Tidsluckan tog bort.',
+ 'Unable to remove this time slot.' => 'Kunde inte ta bort tidsluckan.',
+ 'Do you really want to remove this time slot?' => 'Vill du verkligen ta bort tidsluckan?',
+ 'Remove time slot' => 'Ta bort tidslucka',
+ 'Add new time slot' => 'Lägg till ny tidslucka',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Denna tidslucka används när kryssrutan "hela dagen" är kryssad vid schemalagd ledighet eller övertid.',
+ 'Files' => 'Filer',
+ 'Images' => 'Bilder',
+ 'Private project' => 'Privat projekt',
+ 'Amount' => 'Belopp',
+ 'AUD - Australian Dollar' => 'AUD - Australiska dollar',
+ 'Budget' => 'Budget',
+ 'Budget line' => 'Budgetlinje',
+ 'Budget line removed successfully.' => 'Budgetlinjen togs bort.',
+ 'Budget lines' => 'Budgetlinjer',
+ 'CAD - Canadian Dollar' => 'CAD - Kanadensiska dollar',
+ 'CHF - Swiss Francs' => 'CHF - Schweiziska Franc',
+ 'Cost' => 'Kostnad',
+ 'Cost breakdown' => 'Kostnadssammanställning',
+ 'Custom Stylesheet' => 'Anpassad stilmall',
+ 'download' => 'ladda ned',
+ 'Do you really want to remove this budget line?' => 'Vill du verkligen ta bort budgetlinjen?',
+ 'EUR - Euro' => 'EUR - Euro',
+ 'Expenses' => 'Utgifter',
+ 'GBP - British Pound' => 'GBP - Brittiska Pund',
+ 'INR - Indian Rupee' => 'INR - Indiska Rupier',
+ 'JPY - Japanese Yen' => 'JPY - Japanska Yen',
+ 'New budget line' => 'Ny budgetlinje',
+ 'NZD - New Zealand Dollar' => 'NZD - Nya Zeeländska Dollar',
+ 'Remove a budget line' => 'Ta bort en budgetlinje',
+ 'Remove budget line' => 'Ta bort budgetlinje',
+ 'RSD - Serbian dinar' => 'RSD - Serbiska Dinarer',
+ 'The budget line have been created successfully.' => 'Budgetlinjen har skapats.',
+ 'Unable to create the budget line.' => 'Kunde inte skapa budgetlinjen.',
+ 'Unable to remove this budget line.' => 'Kunde inte ta bort budgetlinjen.',
+ 'USD - US Dollar' => 'USD - Amerikanska Dollar',
+ 'Remaining' => 'Återstående',
+ 'Destination column' => 'Målkolumn',
+ 'Move the task to another column when assigned to a user' => 'Flytta uppgiften till en annan kolumn när den tilldelats en användare',
+ 'Move the task to another column when assignee is cleared' => 'Flytta uppgiften till en annan kolumn när tilldelningen tas bort.',
+ 'Source column' => 'Källkolumn',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ 'Transitions' => 'Övergångar',
+ 'Executer' => 'Verkställare',
+ 'Time spent in the column' => 'Tid i kolumnen.',
+ 'Task transitions' => 'Uppgiftsövergångar',
+ 'Task transitions export' => 'Export av uppgiftsövergångar',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Denna rapport innehåller alla kolumnförflyttningar för varje uppgift med datum, användare och nedlagd tid vid varje övergång.',
+ 'Currency rates' => 'Valutakurser',
+ 'Rate' => 'Kurs',
+ 'Change reference currency' => 'Ändra referenskurs',
+ 'Add a new currency rate' => 'Lägg till ny valutakurs',
+ 'Currency rates are used to calculate project budget.' => 'Valutakurser används för att beräkna projektbudget.',
+ 'Reference currency' => 'Referensvaluta',
+ 'The currency rate have been added successfully.' => 'Valutakursen har lagts till.',
+ 'Unable to add this currency rate.' => 'Kunde inte lägga till valutakursen.',
+ 'Send notifications to a Slack channel' => 'Skicka notiser till en Slack kanal',
+ 'Webhook URL' => 'Webhook URL',
+ 'Help on Slack integration' => 'Hjälp för Slack integration',
+ '%s remove the assignee of the task %s' => '%s ta bort tilldelningen av uppgiften %s',
+ 'Send notifications to Hipchat' => 'Skicka notiser till Hipchat',
+ 'API URL' => 'API URL',
+ 'Room API ID or name' => 'Room API ID eller namn',
+ 'Room notification token' => 'Room notistoken',
+ 'Help on Hipchat integration' => 'Hjälp för Hipchat integration',
+ 'Enable Gravatar images' => 'Aktivera Gravatar bilder',
+ 'Information' => 'Information',
+ 'Check two factor authentication code' => 'Kolla tvåfaktorsverifieringskod',
+ 'The two factor authentication code is not valid.' => 'Tvåfaktorsverifieringskoden är inte giltig.',
+ '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',
+ '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).',
+ // 'Screenshot taken %s' => '',
+ // 'Add a screenshot' => '',
+ // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
+ // 'Screenshot uploaded successfully.' => '',
+ 'SEK - Swedish Krona' => 'SEK - Svensk Krona',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
new file mode 100644
index 00000000..0c5a06dd
--- /dev/null
+++ b/app/Locale/th_TH/translations.php
@@ -0,0 +1,925 @@
+<?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' => 'สีเทา',
+ 'Save' => 'บันทึก',
+ 'Login' => 'เข้าสู่ระบบ',
+ 'Official website:' => 'เวบไซต์อย่างเป็นทางการ:',
+ 'Unassigned' => 'ไม่กำหนด',
+ 'View this task' => 'รายละเอียดงานนี้',
+ 'Remove user' => 'เอาผู้ใช้ออก',
+ 'Do you really want to remove this user: "%s"?' => 'คุณต้องการเอาผู้ใช้ « %s » ออกใช่หรือไม่?',
+ 'New user' => 'ผู้ใช้ใหม่',
+ 'All users' => 'ผู้ใช้ทั้งหมด',
+ 'Username' => 'ชื่อผู้ใช้',
+ 'Password' => 'รหัสผ่าน',
+ 'Default project' => 'โปรเจคเริ่มต้น',
+ 'Administrator' => 'ผู้ดูแลระบบ',
+ 'Sign in' => 'เข้าสู่ระบบ',
+ 'Users' => 'ผู้ใช้',
+ 'No user' => 'ไม่มีผู้ใช้',
+ 'Forbidden' => 'ไม่อนุญาติ',
+ 'Access Forbidden' => 'ไม่อนุญาติให้เข้า',
+ 'Only administrators can access to this page.' => 'หน้าสำหรับผู้ดูแลระบบเท่านั้น',
+ 'Edit user' => 'แก้ไขผู้ใช้',
+ 'Logout' => 'ออกจากระบบ',
+ 'Bad username or password' => 'ชื่อผู้ใช่หรือรหัสผ่านผิด',
+ 'users' => 'ผู้ใช้',
+ 'projects' => 'โปรเจค',
+ 'Edit project' => 'แก้ไขโปรเจค',
+ 'Name' => 'ชื่อ',
+ 'Activated' => 'เปิดใช้งาน',
+ 'Projects' => 'โปรเจค',
+ 'No project' => 'ไม่มีโปรเจค',
+ 'Project' => 'โปรเจค',
+ 'Status' => 'สถานะ',
+ 'Tasks' => 'งาน',
+ 'Board' => 'บอร์ด',
+ 'Actions' => 'การกระทำ',
+ 'Inactive' => 'ไม่เปิดใช้งาน',
+ 'Active' => 'เปิดใช้งาน',
+ 'Column %d' => 'คอลัมน์ %d',
+ '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' => 'ลบโปรเจค',
+ 'Boards' => 'บอร์ด',
+ 'Edit the board for "%s"' => 'แก้ไขบอร์ดสำหรับ « %s »',
+ 'All projects' => 'โปรเจคทั้งหมด',
+ 'Change columns' => 'เปลี่ยนคอลัมน์',
+ 'Add a new column' => 'เพิ่มคอลัมน์ใหม่',
+ 'Title' => 'หัวเรื่อง',
+ 'Add Column' => 'เพิ่มคอลัมน์',
+ 'Project "%s"' => 'โปรเจค « %s »',
+ '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:' => '',
+ 'API token:' => 'API token:',
+ 'More information' => 'ข้อมูลเพิ่มเติม',
+ 'Database size:' => 'ขนาดฐานข้อมูล:',
+ 'Download the database' => 'ดาวน์โหลดฐานข้อมูล',
+ 'Optimize the database' => 'ปรับปรุงฐานข้อมูล',
+ '(VACUUM command)' => '(VACUUM command)',
+ '(Gzip compressed Sqlite file)' => '(Gzip compressed Sqlite file)',
+ 'User settings' => 'ตั้งค่าผู้ใช้',
+ 'My default project:' => 'โปรเจคเริ่มต้นของฉัน:',
+ 'Close a task' => 'ปิดงาน',
+ 'Do you really want to close this task: "%s"?' => 'คุณต้องการปิดงาน « %s » ใช่หรือไม่?',
+ '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' => 'กลับไปที่บอร์ด',
+ '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.' => 'ไม่มีคำอธิบาย',
+ '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 username must be alphanumeric' => 'ชื่อผู้ใช้ต้องเป็นตัวอักษรหรือตัวเลข',
+ 'The user id is required' => 'ต้องการไอดีผู้ใช้',
+ 'Passwords don\'t match' => 'รหัสผ่านไม่ถูกต้อง',
+ 'The confirmation is required' => 'ต้องการการยืนยัน',
+ 'The column is required' => 'ต้องการคอลัมน์',
+ 'The project is required' => 'ต้องการโปรเจค',
+ 'The color is required' => 'ต้องการสี',
+ 'The id is required' => 'ต้องการไอดี',
+ 'The project id is required' => 'ต้องการไอดีโปรเจค',
+ 'The project name is required' => 'ต้องการชื่อโปรเจค',
+ 'This project must be unique' => 'ชื่อโปรเจคต้องไม่ซ้ำ',
+ 'The title is required' => 'ต้องการหัวเรื่อง',
+ 'The language is required' => 'ต้องการภาษา',
+ 'There is no active project, the first step is to create a new project.' => 'ไม่มีโปรเจคที่ทำงานอยู่, ต้องการสร้างโปรเจคใหม่',
+ '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:' => 'แอพเวอร์ชัน:',
+ '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' => 'ไอดี',
+ 'No task' => 'ไม่มีงาน',
+ 'Completed tasks' => 'งานที่เสร็จแล้ว',
+ 'List of projects' => 'รายชื่อโปรเจค',
+ 'Completed tasks for "%s"' => 'งานที่เสร็จแล้วสำหรับ « %s »',
+ '%d closed tasks' => '%d งานที่ปิด',
+ 'No task for this project' => 'ไม่มีงานสำหรับโปรเจคนี้',
+ 'Public link' => 'ลิงค์สาธารณะ',
+ 'There is no column in your project!' => 'ไม่มีคอลัมน์ในโปรเจคของคุณ',
+ 'Change assignee' => 'เปลี่ยนการกำหนด',
+ 'Change assignee for the task "%s"' => 'เปลี่ยนการกำหนดสำหรับงาน « %s »',
+ 'Timezone' => 'เขตเวลา',
+ 'Sorry, I didn\'t find this information in my database!' => 'เสียใจด้วย ไม่สามารถหาข้อมูลในฐานข้อมูลได้',
+ 'Page not found' => 'ไม่พบหน้า',
+ 'Complexity' => 'ความซับซ้อน',
+ 'limit' => 'จำกัด',
+ 'Task limit' => 'จำกัดงาน',
+ // 'Task count' => '',
+ 'This value must be greater than %d' => 'ค่าต้องมากกว่า %d',
+ 'Edit project access list' => 'แก้ไขการเข้าถึงรายชื่อโปรเจค',
+ 'Edit users access' => 'แก้ไขการเข้าถึงผู้ใช้',
+ 'Allow this user' => 'อนุญาตผู้ใช้นี้',
+ 'Only those users have access to this project:' => 'ผู้ใช้ที่สามารถเข้าถึงโปรเจคนี้:',
+ 'Don\'t forget that administrators have access to everything.' => 'อย่าลืมผู้ดูแลระบบสามารถเข้าถึงได้ทุกอย่าง',
+ 'Revoke' => 'ยกเลิก',
+ 'List of authorized users' => 'รายชื่อผู้ใช้ที่ได้รับการยืนยัน',
+ 'User' => 'ผู้ใช้',
+ // 'Nobody have access to this project.' => '',
+ 'You are not allowed to access to this project.' => 'คุณไม่ได้รับอนุญาตให้เข้าถึงโปรเจคนี้',
+ 'Comments' => 'ความคิดเห็น',
+ 'Post comment' => 'แสดงความคิดเห็น',
+ 'Write your text in Markdown' => 'เขียนข้อความในรูปแบบ Markdown',
+ 'Leave a comment' => 'ออกความคิดเห็น',
+ 'Comment is required' => 'ต้องการความคิดเห็น',
+ 'Leave a description' => 'แสดงคำอธิบาย',
+ 'Comment added successfully.' => 'เพิ่มความคิดเห็นเรียบร้อยแล้ว',
+ 'Unable to create your comment.' => 'ไม่สามารถสร้างความคิดเห็น',
+ 'The description is required' => 'ต้องการคำอธิบาย',
+ '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.' => 'ไม่สามารถสร้างการกระทำอัตโนมัติได้',
+ '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' => 'ลบการกระทำอัตโนมัติ',
+ 'Close the task' => 'ปิดงาน',
+ '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' => 'ย้ายงานไปคอลัมน์อื่น',
+ 'Move a task to another position in the same column' => 'ย้ายงานไปตำแหน่งอื่นในคอลัมน์เดียวกัน',
+ 'Task modification' => 'แก้ไขงาน',
+ 'Task creation' => 'สร้างงาน',
+ 'Open a closed task' => 'เปิดงานที่ปิดอยู่',
+ 'Closing a task' => 'กำลังปิดงาน',
+ 'Assign a color to a specific user' => 'กำหนดสีให้ผู้ใช้แบบเจาะจง',
+ 'Column title' => 'หัวเรื่องคอลัมน์',
+ 'Position' => 'ตำแหน่ง',
+ 'Move Up' => 'ย้ายขึ้น',
+ 'Move Down' => 'ย้ายลง',
+ 'Duplicate to another project' => 'ทำซ้ำในโปรเจคอื่น',
+ 'Duplicate' => 'ทำซ้ำ',
+ 'link' => 'ลิงค์',
+ 'Update this comment' => 'ปรับปรุงความคิดเห็นนี้',
+ '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?' => 'คุณต้องการลบความคิดเห็น',
+ 'Only administrators or the creator of the comment can access to this page.' => 'เฉพาะผู้ดูแลระบบหรือผู้สร้างความคิดเห็นเข้าถึงหน้านี้',
+ 'Details' => 'รายละเอียด',
+ 'Current password for the user "%s"' => 'รหัสผ่านปัจจุบันของผู้ใช้ « %s »',
+ 'The current password is required' => 'ต้องการรหัสผ่านปัจจุบัน',
+ 'Wrong password' => 'รหัสผ่านผิด',
+ 'Reset all tokens' => 'รีเซตโทเคนทั้งหมด ',
+ 'All tokens have been regenerated.' => 'โทเคนทั้งหมดทำการสร้างใหม่',
+ 'Unknown' => 'ไม่ทราบ',
+ 'Last logins' => 'เข้าใช้ล่าสุด',
+ 'Login date' => 'วันที่เข้าใข้',
+ 'Authentication method' => 'วิธีการยืนยันตัวตน',
+ 'IP address' => 'ไอพี แอดเดรส',
+ 'User agent' => 'User agent',
+ 'Persistent connections' => 'Persistent connections',
+ 'No session.' => 'No session.',
+ 'Expiration date' => 'หมดอายุวันที่',
+ 'Remember Me' => 'จดจำฉัน',
+ 'Creation date' => 'สร้างวันที่',
+ 'Filter by user' => 'กรองตามผู้ใช้',
+ 'Filter by due date' => 'กรองตามวันครบกำหนด',
+ 'Everybody' => 'ทุกคน',
+ 'Open' => 'เปิด',
+ 'Closed' => 'ปิด',
+ 'Search' => 'ค้นหา',
+ 'Nothing found.' => 'ค้นหาไม่พบ.',
+ 'Search in the project "%s"' => 'ค้นหาในโปรเจค "%s"',
+ 'Due date' => 'วันที่ครบกำหนด',
+ 'Others formats accepted: %s and %s' => 'รูปแบบอื่นที่ได้รับการยอมรับ: %s และ %s',
+ 'Description' => 'คำอธิบาย',
+ '%d comments' => '%d ความคิดเห็น',
+ '%d comment' => '%d ความคิดเห็น',
+ 'Email address invalid' => 'อีเมลผิด',
+ 'Your Google Account is not linked anymore to your profile.' => 'กูเกิลแอคเคาท์ไม่ได้เชื่อมต่อกับประวัติของคุณ',
+ 'Unable to unlink your Google Account.' => 'ไม่สามารถยกเลิกการเชื่อมต่อกับกูเกิลแอคเคาท์',
+ 'Google authentication failed' => 'การยืนยันกับกูเกิลผิดพลาด',
+ 'Unable to link your Google Account.' => 'ไม่สามารถเชื่อมต่อกับกูเกิลแอคเคาท์',
+ 'Your Google 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 #%d' => 'งานที่ %d',
+ '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' => 'ชื่อกลุ่ม',
+ 'Categories for the project "%s"' => 'กลุ่มสำหรับโปรเจค "%s"',
+ 'Add a new category' => 'เพิ่มกลุ่มใหม่',
+ 'Do you really want to remove this category: "%s"?' => 'คุณต้องการลบกลุ่ม "%s" ใช่หรือไม่?',
+ 'Filter by category' => 'กรองตามกลุ่ม',
+ '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" ใช่หรือไม่?',
+ 'open' => 'เปิด',
+ 'Attachments' => 'แนบ',
+ 'Edit the task' => 'แก้ไขงาน',
+ 'Edit the description' => 'แก้ไขคำอธิบาย',
+ 'Add a comment' => 'เพิ่มความคิดเห็น',
+ 'Edit a comment' => 'แก้ไขความคิดเห็น',
+ 'Summary' => 'สรุป',
+ 'Time tracking' => 'การติดตามเวลา',
+ 'Estimate:' => 'ประมาณ:',
+ 'Spent:' => 'ใช้:',
+ 'Do you really want to remove this sub-task?' => 'คุณต้องการลบงานย่อยใช่หรือไม่?',
+ 'Remaining:' => 'เหลือ:',
+ 'hours' => 'ชั่วโมง',
+ 'spent' => 'ใช้',
+ 'estimated' => 'ประมาณ',
+ 'Sub-Tasks' => 'งานย่อย',
+ 'Add a sub-task' => 'เพิ่มงานย่อย',
+ // 'Original estimate' => '',
+ 'Create another sub-task' => 'สร้างงานย่อยอื่น',
+ // 'Time spent' => '',
+ 'Edit a sub-task' => 'แก้ไขงานย่อย',
+ 'Remove a sub-task' => 'ลบงานย่อย',
+ 'The time must be a numeric value' => 'เวลาที่ต้องเป็นตัวเลข',
+ 'Todo' => 'สิ่งที่ต้องทำ',
+ 'In progress' => 'กำลังดำเนินการ',
+ 'Sub-task removed successfully.' => 'ลบงานย่อยเรียบร้อยแล้ว',
+ 'Unable to remove this sub-task.' => 'ไม่สามารถลบงานย่อยได้',
+ 'Sub-task updated successfully.' => 'ปรับปรุงงานย่อย่่เรียบร้อยแล้ว',
+ 'Unable to update your sub-task.' => 'ไม่สามารถปรับปรุงานย่อยได้',
+ 'Unable to create your sub-task.' => 'ไม่สามารถสร้างงานย่อยได้',
+ 'Sub-task added successfully.' => 'เพิ่มงานย่อยเรียบร้อยแล้ว',
+ 'Maximum size: ' => 'ขนาดสูงสุด:',
+ 'Unable to upload the file.' => 'ไม่สามารถอัพโหลดไฟล์ได้',
+ 'Display another project' => 'แสดงโปรเจคอื่น',
+ 'Your GitHub account was successfully linked to your profile.' => 'กิทฮับแอคเคาท์เชื่อมต่อกับประวัติเรียบร้อยแล้ว',
+ 'Unable to link your GitHub Account.' => 'ไม่สามารถเชื่อมต่อกับกิทฮับแอคเคาท์ได้',
+ 'GitHub authentication failed' => 'การยืนยันกิทฮับผิดพลาด',
+ 'Your GitHub account is no longer linked to your profile.' => 'กิทฮับแอคเคาท์ไม่ได้มีการเชื่อมโยงไปยังโปรไฟล์ของคุณ',
+ 'Unable to unlink your GitHub Account.' => 'ไม่สามารถยกเลิกการเชื่อมต่อกิทฮับแอคเคาท์ได้',
+ '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' => 'เริ่มวันที่',
+ 'End Date' => 'สิ้นสุดวันที่',
+ 'Execute' => 'ประมวลผล',
+ 'Task Id' => 'งาน ไอดี',
+ 'Creator' => 'ผู้สร้าง',
+ 'Modification date' => 'วันที่แก้ไข',
+ 'Completion date' => 'วันที่เสร็จสิ้น',
+ 'Clone' => 'เลียนแบบ',
+ // 'Clone Project' => '',
+ 'Project cloned successfully.' => 'เลียนแบบโปรเจคเรียบร้อยแล้ว',
+ 'Unable to clone this project.' => 'ไม่สามารถเลียบแบบโปรเจคได้',
+ 'Email notifications' => 'อีเมลแจ้งเตือน',
+ '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',
+ 'List of due tasks for the project "%s"' => 'รายการงานสำหรับโปรเจค "%s"',
+ // 'New attachment' => '',
+ // 'New comment' => '',
+ // 'New subtask' => '',
+ // 'Subtask updated' => '',
+ // 'Task updated' => '',
+ // 'Task closed' => '',
+ // 'Task opened' => '',
+ '[%s][Due tasks]' => '[%s][งานปัจจุบัน]',
+ '[Kanboard] Notification' => '[Kanboard] แจ้งเตือน',
+ 'I want to receive notifications only for those projects:' => 'ฉันต้องการรับการแจ้งเตือนสำหรับโปรเจค:',
+ 'view the task on Kanboard' => 'แสดงงานบน Kanboard',
+ 'Public access' => 'การเข้าถึงสาธารณะ',
+ // 'Category management' => '',
+ // 'User management' => '',
+ 'Active tasks' => 'งานที่กำลังใช้งาน',
+ 'Disable public access' => 'ปิดการเข้าถึงสาธารณะ',
+ 'Enable public access' => 'เปิดการเข้าถึงสาธารณะ',
+ 'Active projects' => 'เปิดโปรเจค',
+ 'Inactive projects' => 'ปิดโปรเจค',
+ 'Public access disabled' => 'การเข้าถึงสาธารณะถูกปิด',
+ 'Do you really want to disable this project: "%s"?' => 'คุณต้องการปิดการใช้งานโปรเจคนี้: "%s" ใช่หรือไม่?',
+ 'Do you really want to duplicate 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' => 'ปิดการใช้',
+ 'Google account linked' => 'เชื่อมกับกูเกิลแอคเคาท์',
+ 'Github account linked' => 'เชื่อมกับกิทฮับแอคเคาท์',
+ 'Username:' => 'ชื่อผู้ใช้:',
+ 'Name:' => 'ชื่อ:',
+ 'Email:' => 'อีเมล:',
+ 'Default project:' => 'โปรเจคเริ่มต้น:',
+ 'Notifications:' => 'แจ้งเตือน:',
+ 'Notifications' => 'การแจ้งเตือน',
+ 'Group:' => 'กลุ่ม:',
+ 'Regular user' => 'ผู้ใช้ปกติ:',
+ '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' => 'เปลี่ยนกลุ่ม',
+ '%s updated the task %s' => '%s ปรับปรุงงานแล้ว %s',
+ '%s opened the task %s' => '%s เปิดงานแล้ว %s',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s ย้ายงานแล้ว %s ไปตำแหน่ง #%d ในคอลัมน์ "%s"',
+ '%s moved the task %s to the column "%s"' => '%s ย้ายงานแล้ว %s ไปคอลัมน์ "%s"',
+ '%s created the task %s' => '%s สร้างงานแล้ว %s',
+ '%s closed the task %s' => '%s ปิดงานแล้ว %s',
+ '%s created a subtask for the task %s' => '%s สร้างงานย่อยสำหรับงานแล้ว %s',
+ '%s updated a subtask for the task %s' => '%s ปรับปรุงงานย่อยสำหรับงานแล้ว %s',
+ 'Assigned to %s with an estimate of %s/%sh' => 'กำหนดให้ %s โดยประมาณแล้ว %s/%sh',
+ 'Not assigned, estimate of %sh' => 'ไม่กำหนดแล้ว, ประมาณเวลาที่ใช้ %s ชั่วโมง',
+ '%s updated a comment on the task %s' => '%s ปรับปรุงความคิดเห็นในงานแล้ว %s',
+ '%s commented the task %s' => '%s แสดงความคิดเห็นของงานแล้ว %s',
+ '%s\'s activity' => 'กิจกรรม %s',
+ 'No activity.' => 'ไม่มีกิจกรรม',
+ 'RSS feed' => 'RSS feed',
+ '%s updated a comment on the task #%d' => '%s ปรับปรุงความคิดเห็นบนงานแล้ว #%d',
+ '%s commented on the task #%d' => '%s แสดงความคิดเห็นบนงานแล้ว #%d',
+ '%s updated a subtask for the task #%d' => '%s ปรับปรุงงานย่อยสำหรับงานแล้ว #%d',
+ '%s created a subtask for the task #%d' => '%s สร้างงานย่อยสำหรับงานแล้ว #%d',
+ '%s updated the task #%d' => '%s ปรับปรุงงานแล้ว #%d',
+ '%s created the task #%d' => '%s สร้างงานแล้ว #%d',
+ '%s closed the task #%d' => '%s ปิดงานแล้ว #%d',
+ '%s open the task #%d' => '%s เปิดงานแล้ว #%d',
+ '%s moved the task #%d to the column "%s"' => '%s ย้ายงานแล้ว #%d ไปที่คอลัมน์ "%s"',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s ย้ายงานแล้ว #%d ไปตำแหน่ง %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 changed the assignee of the task %s to %s' => '',
+ // 'Column Change' => '',
+ // 'Position Change' => '',
+ // 'Assignee Change' => '',
+ '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' => '',
+ // 'Label' => '',
+ // 'Database' => '',
+ // 'About' => '',
+ // 'Database driver:' => '',
+ // 'Board settings' => '',
+ // 'URL and token' => '',
+ // 'Webhook settings' => '',
+ // 'URL for task creation:' => '',
+ // 'Reset token' => '',
+ // 'API endpoint:' => '',
+ // 'Refresh interval for private board' => '',
+ // 'Refresh interval for public board' => '',
+ // 'Task highlight period' => '',
+ // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
+ // 'Frequency in second (60 seconds by default)' => '',
+ // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
+ // 'Application URL' => '',
+ // 'Example: http://example.kanboard.net/ (used by email notifications)' => '',
+ // 'Token regenerated.' => '',
+ // 'Date format' => '',
+ // 'ISO format is always accepted, example: "%s" and "%s"' => '',
+ // 'New private project' => '',
+ // 'This project is private' => '',
+ // 'Type here to create a new sub-task' => '',
+ // 'Add' => '',
+ // 'Estimated time: %s hours' => '',
+ // 'Time spent: %s hours' => '',
+ // 'Started on %B %e, %Y' => '',
+ // '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' => '',
+ // 'API' => '',
+ // 'Integration' => '',
+ // 'Github webhooks' => '',
+ // 'Help on Github webhooks' => '',
+ // 'Create a comment from an external provider' => '',
+ // 'Github issue comment created' => '',
+ // 'Configure' => '',
+ // 'Project management' => '',
+ // 'My projects' => '',
+ // 'Columns' => '',
+ // 'Task' => '',
+ // 'Your are not member of any project.' => '',
+ // 'Percentage' => '',
+ // 'Number of tasks' => '',
+ // 'Task distribution' => '',
+ // 'Reportings' => '',
+ // 'Task repartition for "%s"' => '',
+ // 'Analytics' => '',
+ // 'Subtask' => '',
+ // 'My subtasks' => '',
+ // 'User repartition' => '',
+ // 'User repartition for "%s"' => '',
+ // 'Clone this project' => '',
+ // 'Column removed successfully.' => '',
+ // 'Edit Project' => '',
+ // 'Github Issue' => '',
+ // 'Not enough data to show the graph.' => '',
+ // 'Previous' => '',
+ // 'The id must be an integer' => '',
+ // 'The project id must be an integer' => '',
+ // 'The status must be an integer' => '',
+ // 'The subtask id is required' => '',
+ // 'The subtask id must be an integer' => '',
+ // 'The task id is required' => '',
+ // 'The task id must be an integer' => '',
+ // 'The user id must be an integer' => '',
+ // 'This value is required' => '',
+ // 'This value must be numeric' => '',
+ // 'Unable to create this task.' => '',
+ // 'Cumulative flow diagram' => '',
+ // 'Cumulative flow diagram for "%s"' => '',
+ // 'Daily project summary' => '',
+ // 'Daily project summary export' => '',
+ // 'Daily project summary export for "%s"' => '',
+ // 'Exports' => '',
+ // 'This export contains the number of tasks per column grouped per day.' => '',
+ // '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"?' => '',
+ // 'Inactive swimlanes' => '',
+ // 'Set project manager' => '',
+ // 'Set project member' => '',
+ // 'Remove a swimlane' => '',
+ // 'Rename' => '',
+ // 'Show default swimlane' => '',
+ // 'Swimlane modification for the project "%s"' => '',
+ // 'Swimlane not found.' => '',
+ // 'Swimlane removed successfully.' => '',
+ // 'Swimlanes' => '',
+ // 'Swimlane updated successfully.' => '',
+ // 'The default swimlane have been updated successfully.' => '',
+ // 'Unable to 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' => '',
+ // 'Subtasks exportation for "%s"' => '',
+ // 'Task Title' => '',
+ // 'Untitled' => '',
+ // 'Application default' => '',
+ // 'Language:' => '',
+ // 'Timezone:' => '',
+ // 'All columns' => '',
+ // 'Calendar for "%s"' => '',
+ // 'Filter by column' => '',
+ // 'Filter by status' => '',
+ // 'Calendar' => '',
+ // 'Next' => '',
+ // '#%d' => '',
+ // 'Filter by color' => '',
+ // 'Filter by swimlane' => '',
+ // 'All swimlanes' => '',
+ // 'All colors' => '',
+ // 'All status' => '',
+ // 'Add a comment logging moving the task between columns' => '',
+ // 'Moved to column %s' => '',
+ // 'Change description' => '',
+ // 'User dashboard' => '',
+ // 'Allow only one subtask in progress at the same time for a user' => '',
+ // 'Edit column "%s"' => '',
+ // 'Enable time tracking for subtasks' => '',
+ // 'Select the new status of the subtask: "%s"' => '',
+ // 'Subtask timesheet' => '',
+ // 'There is nothing to show.' => '',
+ // 'Time Tracking' => '',
+ // 'You already have one subtask in progress' => '',
+ // 'Which parts of the project do you want to duplicate?' => '',
+ // 'Change dashboard view' => '',
+ // 'Show/hide activities' => '',
+ // 'Show/hide projects' => '',
+ // 'Show/hide subtasks' => '',
+ // 'Show/hide tasks' => '',
+ // 'Disable login form' => '',
+ // 'Show/hide calendar' => '',
+ // 'User calendar' => '',
+ // 'Bitbucket commit received' => '',
+ // 'Bitbucket webhooks' => '',
+ // 'Help on Bitbucket webhooks' => '',
+ // 'Start' => '',
+ // 'End' => '',
+ // 'Task age in days' => '',
+ // 'Days in this column' => '',
+ // '%dd' => '',
+ // 'Add a link' => '',
+ // 'Add a new link' => '',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ // 'Field required' => '',
+ // 'Link added successfully.' => '',
+ // 'Link updated successfully.' => '',
+ // 'Link removed successfully.' => '',
+ // 'Link labels' => '',
+ // 'Link modification' => '',
+ // 'Links' => '',
+ // 'Link settings' => '',
+ // 'Opposite label' => '',
+ // 'Remove a link' => '',
+ // 'Task\'s links' => '',
+ // 'The labels must be different' => '',
+ // 'There is no link.' => '',
+ // 'This label must be unique' => '',
+ // 'Unable to create your link.' => '',
+ // 'Unable to update your link.' => '',
+ // 'Unable to remove this link.' => '',
+ // 'relates to' => '',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ // 'This task' => '',
+ // '<1h' => '',
+ // '%dh' => '',
+ // '%b %e' => '',
+ // 'Expand tasks' => '',
+ // 'Collapse tasks' => '',
+ // 'Expand/collapse tasks' => '',
+ // 'Close dialog box' => '',
+ // 'Submit a form' => '',
+ // 'Board view' => '',
+ // 'Keyboard shortcuts' => '',
+ // 'Open board switcher' => '',
+ // 'Application' => '',
+ // 'Filter recently updated' => '',
+ // 'since %B %e, %Y at %k:%M %p' => '',
+ // 'More filters' => '',
+ // 'Compact view' => '',
+ // 'Horizontal scrolling' => '',
+ // 'Compact/wide view' => '',
+ // 'No results match:' => '',
+ // 'Remove hourly rate' => '',
+ // 'Do you really want to remove this hourly rate?' => '',
+ // 'Hourly rates' => '',
+ // 'Hourly rate' => '',
+ // 'Currency' => '',
+ // 'Effective date' => '',
+ // 'Add new rate' => '',
+ // 'Rate removed successfully.' => '',
+ // 'Unable to remove this rate.' => '',
+ // 'Unable to save the hourly rate.' => '',
+ // 'Hourly rate created successfully.' => '',
+ // 'Start time' => '',
+ // 'End time' => '',
+ // 'Comment' => '',
+ // 'All day' => '',
+ // 'Day' => '',
+ // 'Manage timetable' => '',
+ // 'Overtime timetable' => '',
+ // 'Time off timetable' => '',
+ // 'Timetable' => '',
+ // 'Work timetable' => '',
+ // 'Week timetable' => '',
+ // 'Day timetable' => '',
+ // 'From' => '',
+ // 'To' => '',
+ // 'Time slot created successfully.' => '',
+ // 'Unable to save this time slot.' => '',
+ // 'Time slot removed successfully.' => '',
+ // 'Unable to remove this time slot.' => '',
+ // 'Do you really want to remove this time slot?' => '',
+ // 'Remove time slot' => '',
+ // 'Add new time slot' => '',
+ // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
+ // 'Files' => '',
+ // 'Images' => '',
+ // 'Private project' => '',
+ // 'Amount' => '',
+ // 'AUD - Australian Dollar' => '',
+ // 'Budget' => '',
+ // 'Budget line' => '',
+ // 'Budget line removed successfully.' => '',
+ // 'Budget lines' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ // 'Cost' => '',
+ // 'Cost breakdown' => '',
+ // 'Custom Stylesheet' => '',
+ // 'download' => '',
+ // 'Do you really want to remove this budget line?' => '',
+ // 'EUR - Euro' => '',
+ // 'Expenses' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'New budget line' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'Remove a budget line' => '',
+ // 'Remove budget line' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'The budget line have been created successfully.' => '',
+ // 'Unable to create the budget line.' => '',
+ // 'Unable to remove this budget line.' => '',
+ // 'USD - US Dollar' => '',
+ // 'Remaining' => '',
+ // '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' => '',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ // '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' => '',
+ // 'Currency rates are used to calculate project budget.' => '',
+ // 'Reference currency' => '',
+ // 'The currency rate have been added successfully.' => '',
+ // 'Unable to add this currency rate.' => '',
+ // 'Send notifications to a Slack channel' => '',
+ // 'Webhook URL' => '',
+ // 'Help on Slack integration' => '',
+ // '%s remove the assignee of the task %s' => '',
+ // 'Send notifications to Hipchat' => '',
+ // 'API URL' => '',
+ // 'Room API ID or name' => '',
+ // 'Room notification token' => '',
+ // 'Help on Hipchat integration' => '',
+ // '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.' => '',
+ // 'SEK - Swedish Krona' => '',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
+);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
new file mode 100644
index 00000000..f3249ae2
--- /dev/null
+++ b/app/Locale/tr_TR/translations.php
@@ -0,0 +1,925 @@
+<?php
+
+return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
+ 'None' => 'Hiçbiri',
+ 'edit' => 'düzenle',
+ 'Edit' => 'Düzenle',
+ 'remove' => 'sil',
+ 'Remove' => 'Sil',
+ 'Update' => 'Güncelle',
+ 'Yes' => 'Evet',
+ 'No' => 'Hayır',
+ 'cancel' => 'İptal',
+ 'or' => 'veya',
+ 'Yellow' => 'Sarı',
+ 'Blue' => 'Mavi',
+ 'Green' => 'Yeşil',
+ 'Purple' => 'Mor',
+ 'Red' => 'Kırmızı',
+ 'Orange' => 'Turuncu',
+ 'Grey' => 'Gri',
+ 'Save' => 'Kaydet',
+ 'Login' => 'Giriş',
+ 'Official website:' => 'Resmi internet sitesi:',
+ 'Unassigned' => 'Atanmamış',
+ 'View this task' => 'Bu görevi görüntüle',
+ 'Remove user' => 'Kullanıcıyı kaldır',
+ 'Do you really want to remove this user: "%s"?' => 'Bu kullanıcıyı gerçekten silmek istiyor musunuz: "%s"?',
+ 'New user' => 'Yeni kullanıcı',
+ 'All users' => 'Tüm kullanıcılar',
+ 'Username' => 'Kullanıcı adı',
+ 'Password' => 'Şifre',
+ // 'Default project' => '',
+ 'Administrator' => 'Yönetici',
+ 'Sign in' => 'Giriş yap',
+ 'Users' => 'Kullanıcılar',
+ 'No user' => 'Kullanıcı yok',
+ 'Forbidden' => 'Yasak',
+ 'Access Forbidden' => 'Erişim yasak',
+ 'Only administrators can access to this page.' => 'Bu sayfaya yalnızca yöneticiler erişebilir.',
+ 'Edit user' => 'Kullanıcıyı düzenle',
+ 'Logout' => 'Çıkış yap',
+ 'Bad username or password' => 'Hatalı kullanıcı adı veya şifre',
+ 'users' => 'kullanıcılar',
+ 'projects' => 'projeler',
+ 'Edit project' => 'Projeyi düzenle',
+ 'Name' => 'İsim',
+ 'Activated' => 'Aktif',
+ 'Projects' => 'Projeler',
+ 'No project' => 'Proje yok',
+ 'Project' => 'Proje',
+ 'Status' => 'Durum',
+ 'Tasks' => 'Görevler',
+ 'Board' => 'Tablo',
+ 'Actions' => 'İşlemler',
+ 'Inactive' => 'Aktif değil',
+ 'Active' => 'Aktif',
+ 'Column %d' => 'Sütun %d',
+ 'Add this column' => 'Bu sütunu ekle',
+ '%d tasks on the board' => '%d görev bu tabloda',
+ '%d tasks in total' => '%d görev toplam',
+ 'Unable to update this board.' => 'Bu tablo güncellenemiyor.',
+ 'Edit board' => 'Tabloyu düzenle',
+ 'Disable' => 'Devre dışı bırak',
+ 'Enable' => 'Etkinleştir',
+ 'New project' => 'Yeni proje',
+ 'Do you really want to remove this project: "%s"?' => 'Bu projeyi gerçekten silmek istiyor musunuz: "%s"?',
+ 'Remove project' => 'Projeyi sil',
+ 'Boards' => 'Tablolar',
+ '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',
+ 'Add Column' => 'Sütun ekle',
+ 'Project "%s"' => 'Proje "%s"',
+ 'Nobody assigned' => 'Kullanıcı atanmamış',
+ 'Assigned to %s' => '%s kullanıcısına atanmış',
+ 'Remove a column' => 'Bir sütunu sil',
+ 'Remove a column from a board' => 'Tablodan bir sütunu sil',
+ 'Unable to remove this column.' => 'Bu sütun silinemiyor.',
+ 'Do you really want to remove this column: "%s"?' => 'Bu sütunu gerçekten silmek istiyor musunuz: "%s"?',
+ 'This action will REMOVE ALL TASKS associated to this column!' => 'Bu komut sütun içindeki TÜM GÖREVLERİ silecek!',
+ 'Settings' => 'Ayarlar',
+ 'Application settings' => 'Uygulama ayarları',
+ 'Language' => 'Dil',
+ // 'Webhook token:' => '',
+ 'API token:' => 'API Token:',
+ 'More information' => 'Daha fazla bilgi',
+ 'Database size:' => 'Veritabanı boyutu :',
+ 'Download the database' => 'Veritabanını indir',
+ 'Optimize the database' => 'Veritabanını optimize et',
+ '(VACUUM command)' => '(VACUUM komutu)',
+ '(Gzip compressed Sqlite file)' => '(Gzip ile sıkıştırılmış Sqlite dosyası)',
+ 'User settings' => 'Kullanıcı ayarları',
+ 'My default project:' => 'Benim varsayılan projem:',
+ 'Close a task' => 'Bir görevi kapat',
+ 'Do you really want to close this task: "%s"?' => 'Bu görevi gerçekten kapatmak istiyor musunuz: "%s"?',
+ 'Edit a task' => 'Bir görevi düzenle',
+ 'Column' => 'Sütun',
+ 'Color' => 'Renk',
+ 'Assignee' => 'Atanan',
+ 'Create another task' => 'Başka bir görev oluştur',
+ 'New task' => 'Nouvelle tâche',
+ '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.',
+ 'Add a new task' => 'Yeni görev ekle',
+ 'The username is required' => 'Kullanıcı adı gerekli',
+ 'The maximum length is %d characters' => 'Maksimum uzunluk %d karakterdir',
+ 'The minimum length is %d characters' => 'Minimum uzunluk %d karakterdir',
+ 'The password is required' => 'Şifre gerekli',
+ 'This value must be an integer' => 'Bu değer bir rakam olmak zorunda',
+ 'The username must be unique' => 'Kullanıcı adı daha önceden var',
+ 'The username must be alphanumeric' => 'Kullanıcı adı alfanumerik olmalı (geçersiz karakter var)',
+ 'The user id is required' => 'Kullanıcı kodu gerekli',
+ 'Passwords don\'t match' => 'Şifreler uyuşmuyor',
+ 'The confirmation is required' => 'Onay gerekli',
+ 'The column is required' => 'Sütun gerekli',
+ 'The project is required' => 'Proje gerekli',
+ 'The color is required' => 'Renk gerekli',
+ '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',
+ 'The language is required' => 'Dil seçimi gerekli',
+ 'There is no active project, the first step is to create a new project.' => 'Aktif bir proje yok. İlk aşama yeni bir proje oluşturmak olmalı.',
+ 'Settings saved successfully.' => 'Ayarlar başarıyla kaydedildi.',
+ 'Unable to save your settings.' => 'Ayarlarınız kaydedilemedi.',
+ 'Database optimization done.' => 'Veritabanı optimizasyonu tamamlandı.',
+ 'Your project have been created successfully.' => 'Projeniz başarıyla oluşturuldu.',
+ 'Unable to create your project.' => 'Proje oluşturulamadı.',
+ 'Project updated successfully.' => 'Proje başarıyla güncellendi.',
+ 'Unable to update this project.' => 'Bu proje güncellenemedi.',
+ 'Unable to remove this project.' => 'Bu proje silinemedi.',
+ 'Project removed successfully.' => 'Proje başarıyla silindi.',
+ 'Project activated successfully.' => 'Proje başarıyla aktive edildi.',
+ 'Unable to activate this project.' => 'Bu proje aktive edilemedi.',
+ 'Project disabled successfully.' => 'Proje devre dışı bırakıldı.',
+ 'Unable to disable this project.' => 'Bu proje devre dışı bırakılamadı.',
+ 'Unable to open this task.' => 'Bu görev açılamıyor.',
+ 'Task opened successfully.' => 'Görev başarıyla açıldı.',
+ 'Unable to close this task.' => 'Bu görev kapatılamıyor.',
+ 'Task closed successfully.' => 'Görev başarıyla kapatıldı.',
+ 'Unable to update your task.' => 'Görev güncellenemiyor.',
+ 'Task updated successfully.' => 'Görev başarıyla güncellendi.',
+ 'Unable to create your task.' => 'Görev oluşturulamadı.',
+ 'Task created successfully.' => 'Görev başarıyla oluşturuldu.',
+ 'User created successfully.' => 'Kullanıcı başarıyla oluşturuldu',
+ 'Unable to create your user.' => 'Kullanıcı oluşturulamıyor.',
+ 'User updated successfully.' => 'Kullanıcı başarıyla güncellendi.',
+ 'Unable to update your user.' => 'Kullanıcı güncellenemiyor.',
+ 'User removed successfully.' => 'Kullanıcı silindi.',
+ 'Unable to remove this user.' => 'Bu kullanıcı silinemiyor.',
+ 'Board updated successfully.' => 'Tablo başarıyla güncellendi.',
+ 'Ready' => 'Hazır',
+ 'Backlog' => 'Bekleme listesi',
+ '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',
+ 'No task' => 'Görev yok',
+ 'Completed tasks' => 'Tamamlanan görevler',
+ 'List of projects' => 'Proje listesi',
+ 'Completed tasks for "%s"' => '"%s" için tamamlanan görevler',
+ '%d closed tasks' => '%d kapatılmış görevler',
+ // 'No task for this project' => '',
+ 'Public link' => 'Dışa açık link',
+ 'There is no column in your project!' => 'Projenizde hiç sütun yok',
+ '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!' => '',
+ 'Page not found' => 'Sayfa bulunamadı',
+ 'Complexity' => 'Zorluk seviyesi',
+ 'limit' => 'limit',
+ 'Task limit' => 'Görev limiti',
+ 'Task count' => 'Görev sayısı',
+ 'This value must be greater than %d' => 'Bu değer %d den büyük olmalı',
+ 'Edit project access list' => 'Proje erişim listesini düzenle',
+ 'Edit users access' => 'Kullanıcı erişim haklarını düzenle',
+ 'Allow this user' => 'Bu kullanıcıya izin ver',
+ 'Only those users have access to this project:' => 'Bu projeye yalnızca şu kullanıcılar erişebilir:',
+ '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.',
+ 'You are not allowed to access to this project.' => 'Bu projeye giriş yetkiniz yok.',
+ 'Comments' => 'Yorumlar',
+ 'Post comment' => 'Yorum ekle',
+ 'Write your text in Markdown' => 'Yazınızı Markdown ile yazın',
+ 'Leave a comment' => 'Bir yorum ekle',
+ 'Comment is required' => 'Yorum gerekli',
+ 'Leave a description' => 'Açıklama ekleyin',
+ 'Comment added successfully.' => 'Yorum eklendi',
+ 'Unable to create your comment.' => 'Yorumunuz oluşturulamadı',
+ 'The description is required' => 'Açıklama gerekli',
+ 'Edit this task' => 'Bu görevi değiştir',
+ 'Due Date' => 'Termin',
+ '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ı',
+ 'Remove an action' => 'Bir işlemi sil',
+ 'Unable to remove this action.' => 'Bu işlem silinemedi',
+ 'Action removed successfully.' => 'İşlem başarıyla silindi',
+ 'Automatic actions for the project "%s"' => '"%s" projesi için otomatik işlemler',
+ 'Defined actions' => 'Tanımlanan işlemler',
+ 'Add an action' => 'İşlem ekle',
+ 'Event name' => 'Durum adı',
+ 'Action name' => 'İşlem adı',
+ 'Action parameters' => 'İşlem parametreleri',
+ 'Action' => 'İşlem',
+ 'Event' => 'Durum',
+ 'When the selected event occurs execute the corresponding action.' => 'Seçilen durum oluştuğunda ilgili eylemi gerçekleştir.',
+ 'Next step' => 'Sonraki adım',
+ 'Define action parameters' => 'İşlem parametrelerini düzenle',
+ 'Save this action' => 'Bu işlemi kaydet',
+ 'Do you really want to remove this action: "%s"?' => 'Bu işlemi silmek istediğinize emin misiniz: "%s"?',
+ 'Remove an automatic action' => 'Bir otomatik işlemi sil',
+ 'Close the task' => 'Görevi kapat',
+ 'Assign the task to a specific user' => 'Görevi bir kullanıcıya ata',
+ 'Assign the task to the person who does the action' => 'Görevi, işlemi gerçekleştiren kullanıcıya ata',
+ 'Duplicate the task to another project' => 'Görevi bir başka projeye kopyala',
+ 'Move a task to another column' => 'Bir görevi başka bir sütuna taşı',
+ 'Move a task to another position in the same column' => 'Bir görevin aynı sütunda yerini değiştir',
+ 'Task modification' => 'Görev düzenleme',
+ 'Task creation' => 'Görev oluşturma',
+ 'Open a closed task' => 'Kapalı bir görevi aç',
+ 'Closing a task' => 'Bir görev kapatılıyor',
+ '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',
+ 'Update this comment' => 'Bu yorumu güncelle',
+ '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.',
+ 'Details' => 'Detaylar',
+ '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',
+ 'Reset all tokens' => 'Tüm fişleri sıfırla',
+ 'All tokens have been regenerated.' => 'Tüm fişler yeniden oluşturuldu.',
+ 'Unknown' => 'Bilinmeyen',
+ 'Last logins' => 'Son kullanıcı girişleri',
+ 'Login date' => 'Giriş tarihi',
+ 'Authentication method' => 'Doğrulama yöntemi',
+ 'IP address' => 'IP adresi',
+ 'User agent' => 'Kullanıcı sistemi',
+ 'Persistent connections' => 'Kalıcı bağlantılar',
+ // 'No session.' => '',
+ 'Expiration date' => 'Geçerlilik sonu',
+ 'Remember Me' => 'Beni hatırla',
+ 'Creation date' => 'Oluşturulma tarihi',
+ 'Filter by user' => 'Kullanıcıya göre filtrele',
+ 'Filter by due date' => 'Termine göre filtrele',
+ 'Everybody' => 'Herkes',
+ 'Open' => 'Açık',
+ 'Closed' => 'Kapalı',
+ 'Search' => 'Ara',
+ 'Nothing found.' => 'Hiçbir şey bulunamadı',
+ 'Search in the project "%s"' => '"%s" Projesinde ara',
+ 'Due date' => 'Termin',
+ 'Others formats accepted: %s and %s' => 'Diğer kabul edilen formatlar: %s ve %s',
+ 'Description' => 'Açıklama',
+ '%d comments' => '%d yorumlar',
+ '%d comment' => '%d yorum',
+ 'Email address invalid' => 'E-Posta adresi geçersiz',
+ 'Your Google Account is not linked anymore to your profile.' => 'Google hesabınız artık profilinize bağlı değil',
+ 'Unable to unlink your Google Account.' => 'Google hesabınızla bağ koparılamadı',
+ 'Google authentication failed' => 'Google hesap doğrulaması başarısız',
+ 'Unable to link your Google Account.' => 'Google hesabınızla bağ oluşturulamadı',
+ 'Your Google Account is linked to your profile successfully.' => 'Google hesabınız profilinize başarıyla bağlandı',
+ 'Email' => 'E-Posta',
+ 'Link my Google Account' => 'Google hesabımla bağ oluştur',
+ 'Unlink my Google Account' => 'Google hesabımla bağı kaldır',
+ 'Login with my Google Account' => 'Google hesabımla giriş yap',
+ 'Project not found.' => 'Proje bulunamadı',
+ 'Task #%d' => 'Görev #%d',
+ 'Task removed successfully.' => 'Görev 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',
+ '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',
+ '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 Name' => 'Kategori adı',
+ 'Categories for the project "%s"' => '"%s" Projesi için kategoriler',
+ 'Add a new category' => 'Yeni kategori ekle',
+ 'Do you really want to remove this category: "%s"?' => 'Bu kategoriyi silmek istediğinize emin misiniz: "%s"?',
+ 'Filter by category' => 'Kategoriye göre filtrele',
+ 'All categories' => 'Tüm kategoriler',
+ 'No category' => 'Kategori Yok',
+ 'The name is required' => 'İsim gerekli',
+ 'Remove a file' => 'Dosya sil',
+ 'Unable to remove this file.' => 'Dosya silinemedi',
+ 'File removed successfully.' => 'Dosya silindi',
+ 'Attach a document' => 'Dosya ekle',
+ 'Do you really want to remove this file: "%s"?' => 'Bu dosyayı silmek istediğinize emin misiniz: "%s"?',
+ 'open' => 'aç',
+ 'Attachments' => 'Ekler',
+ 'Edit the task' => 'Görevi değiştir',
+ 'Edit the description' => 'Açıklamayı değiştir',
+ 'Add a comment' => 'Yorum ekle',
+ 'Edit a comment' => 'Yorum değiştir',
+ 'Summary' => 'Özet',
+ 'Time tracking' => 'Zaman takibi',
+ 'Estimate:' => 'Tahmini:',
+ 'Spent:' => 'Harcanan:',
+ 'Do you really want to remove this sub-task?' => 'Bu alt görevi silmek istediğinize emin misiniz',
+ 'Remaining:' => 'Kalan',
+ 'hours' => 'saat',
+ 'spent' => 'harcanan',
+ 'estimated' => 'tahmini',
+ 'Sub-Tasks' => 'Alt Görev',
+ 'Add a sub-task' => 'Alt görev ekle',
+ // 'Original estimate' => '',
+ 'Create another sub-task' => 'Başka bir alt görev daha oluştur',
+ // 'Time spent' => '',
+ '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ı',
+ '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',
+ 'Maximum size: ' => 'Maksimum boyutu',
+ 'Unable to upload the file.' => 'Karşıya yükleme başarısız',
+ 'Display another project' => 'Başka bir proje göster',
+ 'Your GitHub account was successfully linked to your profile.' => 'GitHub Hesabınız Profilinize bağlandı.',
+ 'Unable to link your GitHub Account.' => 'GitHub hesabınızla bağ oluşturulamadı.',
+ // 'GitHub authentication failed' => '',
+ // 'Your GitHub account is no longer linked to your profile.' => '',
+ // 'Unable to unlink your GitHub Account.' => '',
+ // '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',
+ 'Creator' => 'Oluşturan',
+ 'Modification date' => 'Değişiklik tarihi',
+ 'Completion date' => 'Tamamlanma tarihi',
+ 'Clone' => 'Kopya oluştur',
+ 'Clone Project' => 'Projenin kopyasını oluştur',
+ 'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.',
+ 'Unable to clone this project.' => 'Proje kopyası oluşturulamadı.',
+ 'Email notifications' => 'E-Posta bilgilendirmesi',
+ '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ı.',
+ 'Sub-task updated' => 'Alt görev güncellendi',
+ 'Title:' => 'Başlık',
+ 'Status:' => 'Durum',
+ 'Assignee:' => 'Sorumlu:',
+ 'Time tracking:' => 'Zaman takibi',
+ 'New sub-task' => 'Yeni alt görev',
+ 'New attachment added "%s"' => 'Yeni dosya "%s" eklendi.',
+ 'Comment updated' => 'Yorum güncellendi',
+ 'New comment posted by %s' => '%s tarafından yeni yorum eklendi',
+ 'List of due tasks for the project "%s"' => '"%s" projesi için ilgili görevlerin listesi',
+ 'New attachment' => 'Yeni dosya eki',
+ 'New comment' => 'Yeni yorum',
+ 'New subtask' => 'Yeni alt görev',
+ 'Subtask updated' => 'Alt görev güncellendi',
+ 'Task updated' => 'Görev güncellendi',
+ 'Task closed' => 'Görev kapatıldı',
+ 'Task opened' => 'Görev açıldı',
+ '[%s][Due tasks]' => '[%s][İlgili görevler]',
+ '[Kanboard] Notification' => '[Kanboard] Bildirim',
+ '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',
+ 'Public access' => 'Dışa açık erişim',
+ 'Category management' => 'Kategori yönetimi',
+ '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ç',
+ 'Active projects' => 'Aktif projeler',
+ 'Inactive projects' => 'Aktif olmayan projeler',
+ 'Public access disabled' => 'Dışa açık erişim kapatıldı',
+ 'Do you really want to disable this project: "%s"?' => 'Bu projeyi devre dışı bırakmak istediğinize emin misiniz?: "%s"',
+ 'Do you really want to duplicate this project: "%s"?' => 'Bu projenin kopyasını oluşturmak istediğinize emin misiniz?: "%s"',
+ 'Do you really want to enable this project: "%s"?' => 'Bu projeyi aktive etmek istediğinize emin misiniz?: "%s"',
+ 'Project activation' => 'Proje aktivasyonu',
+ 'Move the task to another project' => 'Görevi başka projeye taşı',
+ 'Move to another project' => 'Başka projeye taşı',
+ 'Do you really want to duplicate this task?' => 'Bu görevin kopyasını oluşturmak istediğinize emin misiniz?',
+ 'Duplicate a task' => 'Görevin kopyasını oluştur',
+ 'External accounts' => 'Dış hesaplar',
+ 'Account type' => 'Hesap türü',
+ 'Local' => 'Yerel',
+ 'Remote' => 'Uzak',
+ 'Enabled' => 'Etkinleştirildi',
+ 'Disabled' => 'Devre dışı bırakıldı',
+ 'Google account linked' => 'Google hesabıyla bağlı',
+ 'Github account linked' => 'Github hesabıyla bağlı',
+ 'Username:' => 'Kullanıcı adı',
+ 'Name:' => 'Ad',
+ 'Email:' => 'E-Posta',
+ 'Default project:' => 'Varsayılan Proje:',
+ '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.',
+ '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',
+ '%s opened the task %s' => '%s kullanıcısı %s görevini açtı',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s kullanıcısı %s görevini #%d pozisyonu "%s" sütununa taşıdı',
+ '%s moved the task %s to the column "%s"' => '%s kullanıcısı %s görevini "%s" sütununa taşıdı',
+ '%s created the task %s' => '%s kullanıcısı %s görevini oluşturdu',
+ '%s closed the task %s' => '%s kullanıcısı %s görevini kapattı',
+ '%s created a subtask for the task %s' => '%s kullanıcısı %s görevi için bir alt görev oluşturdu',
+ '%s updated a subtask for the task %s' => '%s kullanıcısı %s görevinin bir alt görevini güncelledi',
+ 'Assigned to %s with an estimate of %s/%sh' => '%s kullanıcısına tahmini %s/%s saat tamamlanma süresi ile atanmış',
+ 'Not assigned, estimate of %sh' => 'Kimseye atanmamış, tahmini süre %s saat',
+ '%s updated a comment on the task %s' => '%s kullanıcısı %s görevinde bir yorumu güncelledi',
+ '%s commented the task %s' => '%s kullanıcısı %s görevine yorum ekledi',
+ '%s\'s activity' => '%s\'in aktivitesi',
+ 'No activity.' => 'Aktivite yok.',
+ 'RSS feed' => 'RSS kaynağı',
+ '%s updated a comment on the task #%d' => '%s kullanıcısı #%d nolu görevde bir yorumu güncelledi',
+ '%s commented on the task #%d' => '%s kullanıcısı #%d nolu göreve yorum ekledi',
+ '%s updated a subtask for the task #%d' => '%s kullanıcısı #%d nolu görevin bir alt görevini güncelledi',
+ '%s created a subtask for the task #%d' => '%s kullanıcısı #%d nolu göreve bir alt görev ekledi',
+ '%s updated the task #%d' => '%s kullanıcısı #%d nolu görevi güncelledi',
+ '%s created the task #%d' => '%s kullanıcısı #%d nolu görevi oluşturdu',
+ '%s closed the task #%d' => '%s kullanıcısı #%d nolu görevi kapattı',
+ '%s open the task #%d' => '%s kullanıcısı #%d nolu görevi açtı',
+ '%s moved the task #%d to the column "%s"' => '%s kullanıcısı #%d nolu görevi "%s" sütununa taşıdı',
+ '%s moved the task #%d to the position %d in the column "%s"' => '%s kullanıcısı #%d nolu görevi %d pozisyonu "%s" sütununa taşıdı',
+ 'Activity' => 'Aktivite',
+ 'Default values are "%s"' => 'Varsayılan değerler "%s"',
+ 'Default columns for new projects (Comma-separated)' => 'Yeni projeler için varsayılan sütunlar (virgül ile ayrılmış)',
+ 'Task assignee change' => 'Göreve atanan kullanıcı değişikliği',
+ '%s change the assignee of the task #%d to %s' => '%s kullanıcısı #%d nolu görevin sorumlusunu %s olarak değiştirdi',
+ '%s changed the assignee of the task %s to %s' => '%s kullanıcısı %s görevinin sorumlusunu %s olarak değiştirdi',
+ 'Column Change' => 'Sütun değişikliği',
+ 'Position Change' => 'Konum değişikliği',
+ 'Assignee Change' => 'Sorumlu değişikliği',
+ '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ı',
+ 'About' => 'Hakkında',
+ 'Database driver:' => 'Veri bankası sürücüsü',
+ 'Board settings' => 'Tablo ayarları',
+ 'URL and token' => 'URL veya Token',
+ 'Webhook settings' => 'Webhook ayarları',
+ 'URL for task creation:' => 'Görev oluşturma için URL',
+ 'Reset token' => 'Reset Token',
+ 'API endpoint:' => 'API endpoint',
+ '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',
+ 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Bir görevin yeni değiştirilmiş sayılması için süre (saniye olarak) (Bu özelliği iptal etmek için 0, varsayılan değer 2 gün)',
+ '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.',
+ '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.',
+ 'My tasks' => 'Görevlerim',
+ 'Activity stream' => 'Güncel olay akışı',
+ 'Dashboard' => 'Anasayfa',
+ 'Confirmation' => 'Onay',
+ 'Allow everybody to access to this project' => 'Bu projeye herkesin erişimine izin ver',
+ 'Everybody have access to this project.' => 'Bu projeye herkesin erişimi var.',
+ 'Webhooks' => 'Webhooks',
+ 'API' => 'API',
+ 'Integration' => 'Entegrasyon',
+ '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',
+ 'Configure' => 'Ayarla',
+ 'Project management' => 'Proje yönetimi',
+ 'My projects' => 'Projelerim',
+ 'Columns' => 'Sütunlar',
+ 'Task' => 'Görev',
+ 'Your are not member of any project.' => 'Hiç bir projenin üyesi değilsiniz.',
+ 'Percentage' => 'Yüzde',
+ 'Number of tasks' => 'Görev sayısı',
+ 'Task distribution' => 'Görev dağılımı',
+ 'Reportings' => 'Raporlar',
+ 'Task repartition for "%s"' => '"%s" için görev dağılımı',
+ 'Analytics' => 'Analiz',
+ 'Subtask' => 'Alt görev',
+ 'My subtasks' => 'Alt görevlerim',
+ 'User repartition' => 'Kullanıcı dağılımı',
+ '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ı.',
+ 'Edit Project' => 'Projeyi düzenle',
+ '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ı',
+ 'The project id must be an integer' => 'Proje numarası bir tam sayı olmalı',
+ 'The status must be an integer' => 'Durum bir tam sayı olmalı',
+ 'The subtask id is required' => 'Alt görev numarası gerekli',
+ 'The subtask id must be an integer' => 'Alt görev numarası bir tam sayı olmalı',
+ 'The task id is required' => 'Görev numarası gerekli',
+ 'The task id must be an integer' => 'Görev numarası bir tam sayı olmalı',
+ 'The user id must be an integer' => 'Kullanıcı numarası bir tam sayı olmalı',
+ 'This value is required' => 'Bu değer gerekli',
+ 'This value must be numeric' => 'Bu değer sayı olmalı',
+ 'Unable to create this task.' => 'Bu görev oluşturulamıyor.',
+ 'Cumulative flow diagram' => 'Kümülatif akış diyagramı',
+ 'Cumulative flow diagram for "%s"' => '"%s" için kümülatif akış diyagramı',
+ 'Daily project summary' => 'Günlük proje özeti',
+ 'Daily project summary export' => 'Günlük proje özetini dışa aktar',
+ 'Daily project summary export for "%s"' => '"%s" için günlük proje özetinin dışa',
+ 'Exports' => 'Dışa aktarımlar',
+ 'This export contains the number of tasks per column grouped per day.' => 'Bu dışa aktarım günlük gruplanmış olarak her sütundaki görev sayısını içerir.',
+ 'Nothing to preview...' => 'Önizleme yapılacak bir şey yok ...',
+ 'Preview' => 'Önizleme',
+ 'Write' => 'Değiştir',
+ 'Active swimlanes' => 'Aktif Kulvar',
+ 'Add a new swimlane' => 'Yeni bir Kulvar ekle',
+ 'Change default swimlane' => 'Varsayılan Kulvarı değiştir',
+ '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ı',
+ 'Swimlane removed successfully.' => 'Kulvar başarıyla kaldırıldı.',
+ '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',
+ 'Subtasks exportation for "%s"' => '"%s" için alt görevleri dışa aktarımı',
+ 'Task Title' => 'Görev Başlığı',
+ 'Untitled' => 'Başlıksız',
+ 'Application default' => 'Uygulama varsayılanları',
+ 'Language:' => 'Dil:',
+ 'Timezone:' => 'Saat dilimi:',
+ 'All columns' => 'Tüm sütunlar',
+ 'Calendar for "%s"' => '"%s" için takvim',
+ 'Filter by column' => 'Sütuna göre filtrele',
+ 'Filter by status' => 'Duruma göre filtrele',
+ 'Calendar' => 'Takvim',
+ 'Next' => 'Sonraki',
+ '#%d' => '#%d',
+ 'Filter by color' => 'Renklere göre filtrele',
+ 'Filter by swimlane' => 'Kulvara göre filtrele',
+ 'All swimlanes' => 'Tüm Kulvarlar',
+ 'All colors' => 'Tüm Renkler',
+ 'All status' => 'Tüm Durumlar',
+ 'Add a comment logging moving the task between columns' => 'Sütun değiştiğinde kayıt olarak yorum ekle',
+ 'Moved to column %s' => '%s Sütununa taşındı',
+ 'Change description' => 'Açıklamayı değiştir',
+ 'User dashboard' => 'Kullanıcı Anasayfası',
+ 'Allow only one subtask in progress at the same time for a user' => 'Bir kullanıcı için aynı anda yalnızca bir alt göreve izin ver',
+ 'Edit column "%s"' => '"%s" sütununu değiştir',
+ 'Enable time tracking for subtasks' => 'Alt görevler için zaman takibini etkinleştir',
+ 'Select the new status of the subtask: "%s"' => '"%s" alt görevi için yeni durum seçin.',
+ 'Subtask timesheet' => 'Alt görev için zaman takip tablosu',
+ 'There is nothing to show.' => 'Gösterilecek hiçbir şey yok.',
+ '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?',
+ 'Change dashboard view' => 'Anasayfa görünümünü değiştir',
+ 'Show/hide activities' => 'Aktiviteleri göster/gizle',
+ 'Show/hide projects' => 'Projeleri göster/gizle',
+ 'Show/hide subtasks' => 'Alt görevleri göster/gizle',
+ 'Show/hide tasks' => 'Görevleri göster/gizle',
+ 'Disable login form' => 'Giriş formunu devre dışı bırak',
+ 'Show/hide calendar' => 'Takvimi göster/gizle',
+ 'User calendar' => 'Kullanıcı takvimi',
+ 'Bitbucket commit received' => 'Bitbucket commit alındı',
+ 'Bitbucket webhooks' => 'Bitbucket webhooks',
+ 'Help on Bitbucket webhooks' => 'Bitbucket webhooks için yardım',
+ 'Start' => 'Başlangıç',
+ 'End' => 'Son',
+ 'Task age in days' => 'Görev yaşı gün olarak',
+ 'Days in this column' => 'Bu sütunda geçirilen gün',
+ '%dd' => '%dG',
+ 'Add a link' => 'Link ekle',
+ 'Add a new link' => 'Yeni link ekle',
+ 'Do you really want to remove this link: "%s"?' => '"%s" linkini gerçekten silmek istiyor musunuz?',
+ 'Do you really want to remove this link with task #%d?' => '#%d numaralı görev ile linki gerçekten silmek istiyor musunuz?',
+ 'Field required' => 'Bu alan gerekli',
+ 'Link added successfully.' => 'Link başarıyla eklendi.',
+ 'Link updated successfully.' => 'Link başarıyla güncellendi.',
+ 'Link removed successfully.' => 'Link başarıyla silindi.',
+ 'Link labels' => 'Link etiketleri',
+ 'Link modification' => 'Link değiştirme',
+ 'Links' => 'Links',
+ 'Link settings' => 'Link ayarları',
+ 'Opposite label' => 'Zıt etiket',
+ 'Remove a link' => 'Bir link silmek',
+ 'Task\'s links' => 'Görevin linkleri',
+ 'The labels must be different' => 'Etiketler farklı olmalı',
+ 'There is no link.' => 'Hiç bir bağ yok',
+ 'This label must be unique' => 'Bu etiket tek olmalı',
+ 'Unable to create your link.' => 'Link oluşturulamadı.',
+ 'Unable to update your link.' => 'Link güncellenemiyor.',
+ 'Unable to remove this link.' => 'Link kaldırılamıyor',
+ 'relates to' => 'şununla ilgili',
+ 'blocks' => 'şunu engelliyor',
+ 'is blocked by' => 'şunun tarafından engelleniyor',
+ 'duplicates' => 'şunun kopyasını oluşturuyor',
+ 'is duplicated by' => 'şunun tarafından kopyası oluşturuluyor',
+ 'is a child of' => 'şunun astı',
+ 'is a parent of' => 'şunun üstü',
+ 'targets milestone' => 'şu kilometre taşını hedefliyor',
+ 'is a milestone of' => 'şunun için bir kilometre taşı',
+ 'fixes' => 'düzeltiyor',
+ 'is fixed by' => 'şunun tarafından düzeltildi',
+ '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',
+ 'Close dialog box' => 'İletiyi kapat',
+ 'Submit a form' => 'Formu gönder',
+ 'Board view' => 'Tablo görünümü',
+ 'Keyboard shortcuts' => 'Klavye kısayolları',
+ 'Open board switcher' => 'Tablo seçim listesini aç',
+ 'Application' => 'Uygulama',
+ 'Filter recently updated' => 'Son güncellenenleri göster',
+ 'since %B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p\'den beri',
+ 'More filters' => 'Daha fazla filtre',
+ '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:' => '',
+ // 'Remove hourly rate' => '',
+ // 'Do you really want to remove this hourly rate?' => '',
+ // 'Hourly rates' => '',
+ // 'Hourly rate' => '',
+ // 'Currency' => '',
+ // 'Effective date' => '',
+ // 'Add new rate' => '',
+ // 'Rate removed successfully.' => '',
+ // 'Unable to remove this rate.' => '',
+ // 'Unable to save the hourly rate.' => '',
+ // 'Hourly rate created successfully.' => '',
+ // 'Start time' => '',
+ // 'End time' => '',
+ // 'Comment' => '',
+ // 'All day' => '',
+ // 'Day' => '',
+ // 'Manage timetable' => '',
+ // 'Overtime timetable' => '',
+ // 'Time off timetable' => '',
+ // 'Timetable' => '',
+ // 'Work timetable' => '',
+ // 'Week timetable' => '',
+ // 'Day timetable' => '',
+ // 'From' => '',
+ // 'To' => '',
+ // 'Time slot created successfully.' => '',
+ // 'Unable to save this time slot.' => '',
+ // 'Time slot removed successfully.' => '',
+ // 'Unable to remove this time slot.' => '',
+ // 'Do you really want to remove this time slot?' => '',
+ // 'Remove time slot' => '',
+ // 'Add new time slot' => '',
+ // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
+ // 'Files' => '',
+ // 'Images' => '',
+ // 'Private project' => '',
+ // 'Amount' => '',
+ // 'AUD - Australian Dollar' => '',
+ // 'Budget' => '',
+ // 'Budget line' => '',
+ // 'Budget line removed successfully.' => '',
+ // 'Budget lines' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ // 'Cost' => '',
+ // 'Cost breakdown' => '',
+ // 'Custom Stylesheet' => '',
+ // 'download' => '',
+ // 'Do you really want to remove this budget line?' => '',
+ // 'EUR - Euro' => '',
+ // 'Expenses' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'New budget line' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'Remove a budget line' => '',
+ // 'Remove budget line' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'The budget line have been created successfully.' => '',
+ // 'Unable to create the budget line.' => '',
+ // 'Unable to remove this budget line.' => '',
+ // 'USD - US Dollar' => '',
+ // 'Remaining' => '',
+ // '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' => '',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ // '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' => '',
+ // 'Currency rates are used to calculate project budget.' => '',
+ // 'Reference currency' => '',
+ // 'The currency rate have been added successfully.' => '',
+ // 'Unable to add this currency rate.' => '',
+ // 'Send notifications to a Slack channel' => '',
+ // 'Webhook URL' => '',
+ // 'Help on Slack integration' => '',
+ // '%s remove the assignee of the task %s' => '',
+ // 'Send notifications to Hipchat' => '',
+ // 'API URL' => '',
+ // 'Room API ID or name' => '',
+ // 'Room notification token' => '',
+ // 'Help on Hipchat integration' => '',
+ // '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.' => '',
+ // 'SEK - Swedish Krona' => '',
+ // 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
+ // 'Identifier' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
+);
diff --git a/app/Locales/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index fbaef229..1b2a5ef4 100644
--- a/app/Locales/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -1,6 +1,8 @@
<?php
return array(
+ 'number.decimals_separator' => '.',
+ 'number.thousands_separator' => ',',
'None' => '无',
'edit' => '编辑',
'Edit' => '编辑',
@@ -24,7 +26,7 @@ return array(
'Unassigned' => '未指定',
'View this task' => '查看该任务',
'Remove user' => '移除用户',
- 'Do you really want to remove this user: "%s"?' => '你确定要移除这个用户吗:"%s"?',
+ 'Do you really want to remove this user: "%s"?' => '确定要删除用户"%s"吗?',
'New user' => '新建用户',
'All users' => '所有用户',
'Username' => '用户名',
@@ -63,7 +65,7 @@ return array(
'Disable' => '停用',
'Enable' => '启用',
'New project' => '新建项目',
- 'Do you really want to remove this project: "%s"?' => '你确定要移除该项目吗:"%s"?',
+ 'Do you really want to remove this project: "%s"?' => '确定要移除项目"%s"吗?',
'Remove project' => '移除项目',
'Boards' => '看板',
'Edit the board for "%s"' => '为"%s"修改看板',
@@ -78,7 +80,7 @@ return array(
'Remove a column' => '移除一个栏目',
'Remove a column from a board' => '从看板移除一个栏目',
'Unable to remove this column.' => '无法移除该栏目。',
- 'Do you really want to remove this column: "%s"?' => '你确定要移除该栏目:"%s"吗?',
+ 'Do you really want to remove this column: "%s"?' => '确定要移除栏目"%s"吗?',
'This action will REMOVE ALL TASKS associated to this column!' => '该动作将移除与该栏目相关的所有项目!',
'Settings' => '设置',
'Application settings' => '应用设置',
@@ -182,18 +184,19 @@ return array(
'Change assignee' => '变更负责人',
'Change assignee for the task "%s"' => '更改任务"%s"的负责人',
'Timezone' => '时区',
- 'Sorry, I didn\'t found this information in my database!' => '抱歉,无法在数据库中找到该信息!',
+ 'Sorry, I didn\'t find this information in my database!' => '抱歉,无法在数据库中找到该信息!',
'Page not found' => '页面未找到',
'Complexity' => '复杂度',
'limit' => '限制',
'Task limit' => '任务限制',
+ 'Task count' => '任务数',
'This value must be greater than %d' => '该数值必须大于%d',
'Edit project access list' => '编辑项目存取列表',
'Edit users access' => '编辑用户存取权限',
'Allow this user' => '允许该用户',
'Only those users have access to this project:' => '只有这些用户有该项目的存取权限:',
'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。',
- 'revoke' => '撤销',
+ 'Revoke' => '撤销',
'List of authorized users' => '已授权的用户列表',
'User' => '用户',
'Nobody have access to this project.' => '无用户可以访问此项目.',
@@ -212,6 +215,7 @@ return array(
'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.' => '无法为您创建自动动作。',
@@ -230,7 +234,7 @@ return array(
'Next step' => '下一步',
'Define action parameters' => '定义动作参数',
'Save this action' => '保存该动作',
- 'Do you really want to remove this action: "%s"?' => '确定要移除该动作"%s"吗?',
+ 'Do you really want to remove this action: "%s"?' => '确定要移除动作"%s"吗?',
'Remove an automatic action' => '移除一个自动动作',
'Close the task' => '关闭任务',
'Assign the task to a specific user' => '将该任务指派给一个用户',
@@ -284,17 +288,17 @@ return array(
'Nothing found.' => '没找到。',
'Search in the project "%s"' => '在项目"%s"中查找',
'Due date' => '到期时间',
- 'Others formats accepted: %s and %s' => '允许其他格式:%s 和 %s',
+ 'Others formats accepted: %s and %s' => '可以使用的其它格式:%s 和 %s',
'Description' => '描述',
'%d comments' => '%d个评论',
'%d comment' => '%d个评论',
- 'Email address invalid' => 'Email地址无效',
+ 'Email address invalid' => '电子邮件地址无效',
'Your Google Account is not linked anymore to your profile.' => '您的google帐号不再与您的账户配置关联。',
'Unable to unlink your Google Account.' => '无法去除您google帐号的关联',
'Google authentication failed' => 'google验证失败',
'Unable to link your Google Account.' => '无法关联您的google帐号。',
'Your Google Account is linked to your profile successfully.' => '您的google帐号已成功与账户配置关联。',
- 'Email' => 'Email',
+ 'Email' => '电子邮件',
'Link my Google Account' => '关联我的google帐号',
'Unlink my Google Account' => '去除我的google帐号关联',
'Login with my Google Account' => '用我的google帐号登录',
@@ -322,7 +326,7 @@ return array(
'Category Name' => '分类名称',
'Categories for the project "%s"' => '项目"%s"的分类',
'Add a new category' => '加入新分类',
- 'Do you really want to remove this category: "%s"?' => '您确定要移除分类"%s"吗?',
+ 'Do you really want to remove this category: "%s"?' => '确定要移除分类"%s"吗?',
'Filter by category' => '按分类过滤',
'All categories' => '所有分类',
'No category' => '无分类',
@@ -331,7 +335,7 @@ return array(
'Unable to remove this file.' => '无法移除该文件。',
'File removed successfully.' => '文件成功移除。',
'Attach a document' => '附加文档',
- 'Do you really want to remove this file: "%s"?' => '您确定要移除文件"%s"吗?',
+ 'Do you really want to remove this file: "%s"?' => '确定要移除文件"%s"吗?',
'open' => '打开',
'Attachments' => '附件',
'Edit the task' => '修改任务',
@@ -374,10 +378,10 @@ return array(
'Login with my GitHub Account' => '用Github账号登录',
'Link my GitHub Account' => '链接GitHub账号',
'Unlink my GitHub Account' => '取消GitHub账号链接',
- 'Created by %s' => '创建者:',
+ '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"',
+ 'Tasks exportation for "%s"' => '导出"%s"的任务',
'Start Date' => '开始时间',
'End Date' => '结束时间',
'Execute' => '执行',
@@ -385,8 +389,6 @@ return array(
'Creator' => '创建者',
'Modification date' => '修改日期',
'Completion date' => '完成日期',
- 'Webhook URL for task creation' => '创建任务的Webhook URL',
- 'Webhook URL for task modification' => '修改任务的Webhook URL',
'Clone' => '克隆',
'Clone Project' => '复制项目',
'Project cloned successfully.' => '成功复制项目。',
@@ -406,15 +408,13 @@ return array(
'Comment updated' => '更新了评论',
'New comment posted by %s' => '%s 的新评论',
'List of due tasks for the project "%s"' => '项目"%s"的到期任务列表',
- '[%s][New attachment] %s (#%d)' => '[%s][新附件] %s (#%d)',
- '[%s][New comment] %s (#%d)' => '[%s][新评论] %s (#%d)',
- '[%s][Comment updated] %s (#%d)' => '[%s][评论更新] %s (#%d)',
- '[%s][New subtask] %s (#%d)' => '[%s][新子任务] %s (#%d)',
- '[%s][Subtask updated] %s (#%d)' => '[%s][子任务更新] %s (#%d)',
- '[%s][New task] %s (#%d)' => '[%s][新任务] %s (#%d)',
- '[%s][Task updated] %s (#%d)' => '[%s][任务更新] %s (#%d)',
- '[%s][Task closed] %s (#%d)' => '[%s][任务关闭] %s (#%d)',
- '[%s][Task opened] %s (#%d)' => '[%s][任务开启] %s (#%d)',
+ 'New attachment' => '新建附件',
+ 'New comment' => '新建评论',
+ 'New subtask' => '新建子任务',
+ 'Subtask updated' => '子任务更新',
+ 'Task updated' => '任务更新',
+ 'Task closed' => '任务关闭',
+ 'Task opened' => '任务开启',
'[%s][Due tasks]' => '[%s][到期任务]',
'[Kanboard] Notification' => '[Kanboard] 通知',
'I want to receive notifications only for those projects:' => '我仅需要收到下面项目的通知:',
@@ -467,20 +467,20 @@ return array(
'Unable to change the password.' => '无法修改密码。',
'Change category for the task "%s"' => '变更任务 "%s" 的分类',
'Change category' => '变更分类',
- '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s 更新了任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s 开启了任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s 将任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> 移动到了"%s"的第#%d个位置',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s 移动任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> 到栏目 "%s"',
- '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s 创建了任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s 关闭了任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s 创建了 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>的子任务',
- '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s 更新了 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>的子任务',
+ '%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 移动到了"%s"的第#%d个位置',
+ '%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/%s 小时',
'Not assigned, estimate of %sh' => '未分配,预估需要 %s 小时',
- '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s 更新了任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>的评论',
- '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s 评论了任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s\'s activity' => '%s的活动',
- 'No activity.' => '无活动',
+ '%s updated a comment on the task %s' => '%s 更新了任务 %s的评论',
+ '%s commented the task %s' => '%s 评论了任务 %s',
+ '%s\'s activity' => '%s的动态',
+ 'No activity.' => '无动态',
'RSS feed' => 'RSS 链接',
'%s updated a comment on the task #%d' => '%s 更新了任务 #%d 的评论',
'%s commented on the task #%d' => '%s 评论了任务 #%d',
@@ -492,15 +492,15 @@ return array(
'%s open the task #%d' => '%s 开启了任务 #%d',
'%s moved the task #%d to the column "%s"' => '%s 将任务 #%d 移动到栏目 "%s"',
'%s moved the task #%d to the position %d in the column "%s"' => '%s将任务#%d移动到"%s"的第 %d 列',
- 'Activity' => '活动',
+ 'Activity' => '动态',
'Default values are "%s"' => '默认值为 "%s"',
'Default columns for new projects (Comma-separated)' => '新建项目的默认栏目(用逗号分开)',
'Task assignee change' => '任务分配变更',
'%s change the assignee of the task #%d to %s' => '%s 将任务 #%d 分配给了 %s',
- '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '%s 将任务 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> 分配给 %s',
- '[%s][Column Change] %s (#%d)' => '[%s][栏目变更] %s (#%d)',
- '[%s][Position Change] %s (#%d)' => '[%s][位置变更] %s (#%d)',
- '[%s][Assignee Change] %s (#%d)' => '[%s][任务分配变更] %s (#%d)',
+ '%s changed the assignee of the task %s to %s' => '%s 将任务 %s 分配给 %s',
+ 'Column Change' => '栏目变更',
+ 'Position Change' => '位置变更',
+ 'Assignee Change' => '负责人变更',
'New password for the user "%s"' => '用户"%s"的新密码',
'Choose an event' => '选择一个事件',
'Github commit received' => '收到了Github提交',
@@ -546,9 +546,380 @@ return array(
'Time estimated' => '预计时间',
'There is nothing assigned to you.' => '无任务指派给你。',
'My tasks' => '我的任务',
- 'Activity stream' => '活动流',
+ 'Activity stream' => '动态记录',
'Dashboard' => '面板',
'Confirmation' => '确认',
- // 'Allow everybody to access to this project' => '',
- // 'Everybody have access to this project.' => '',
+ 'Allow everybody to access to this project' => '允许所有人访问此项目',
+ 'Everybody have access to this project.' => '所有人都可以访问此项目',
+ 'Webhooks' => '网络钩子',
+ 'API' => '应用程序接口',
+ 'Integration' => '整合',
+ 'Github webhooks' => 'Github 网络钩子',
+ 'Help on Github webhooks' => 'Github 网络钩子帮助',
+ 'Create a comment from an external provider' => '从外部创建一个评论',
+ 'Github issue comment created' => '已经创建了Github问题评论',
+ 'Configure' => '配置',
+ '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.' => '成功删除了栏目。',
+ 'Edit Project' => '编辑项目',
+ 'Github Issue' => 'Github 任务报告',
+ 'Not enough data to show the graph.' => '数据不足,无法绘图。',
+ 'Previous' => '后退',
+ 'The id must be an integer' => '编号必须为整数',
+ 'The project id must be an integer' => '项目编号必须为整数',
+ 'The status must be an integer' => '状态必须为整数',
+ 'The subtask id is required' => '必须提供子任务编号',
+ 'The subtask id must be an integer' => '子任务编号必须为整数',
+ 'The task id is required' => '需要任务编号',
+ 'The task id must be an integer' => '任务编号必须为整数',
+ 'The user id must be an integer' => '用户编号必须为整数',
+ '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' => '书写',
+ '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.' => '已经成功创建泳道。',
+ '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' => '子任务导出',
+ 'Subtasks exportation for "%s"' => '导出"%s"的子任务',
+ 'Task Title' => '任务标题',
+ 'Untitled' => '无标题',
+ 'Application default' => '程序默认',
+ 'Language:' => '语言:',
+ 'Timezone:' => '时区:',
+ 'All columns' => '全部栏目',
+ 'Calendar for "%s"' => '"%s"的日程表',
+ 'Filter by column' => '按栏目过滤',
+ 'Filter by status' => '按状态过滤',
+ 'Calendar' => '日程表',
+ 'Next' => '前进',
+ '#%d' => '#%d',
+ 'Filter by color' => '按颜色过滤',
+ 'Filter by swimlane' => '按泳道过滤',
+ 'All swimlanes' => '全部泳道',
+ 'All colors' => '全部颜色',
+ 'All status' => '全部状态',
+ 'Add a comment logging moving the task between columns' => '在不同栏目间移动任务时添加一个评论',
+ '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"',
+ 'Enable time tracking for subtasks' => '启用子任务的时间记录',
+ '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?' => '要复制项目的哪些内容?',
+ 'Change dashboard view' => '修改仪表板视图',
+ 'Show/hide activities' => '显示/隐藏活动',
+ 'Show/hide projects' => '显示/隐藏项目',
+ 'Show/hide subtasks' => '显示/隐藏子任务',
+ 'Show/hide tasks' => '显示/隐藏任务',
+ 'Disable login form' => '禁用登录界面',
+ 'Show/hide calendar' => '显示/隐藏日程表',
+ 'User calendar' => '用户日程表',
+ 'Bitbucket commit received' => '收到Bitbucket提交',
+ 'Bitbucket webhooks' => 'Bitbucket网络钩子',
+ 'Help on Bitbucket webhooks' => 'Bitbucket网络钩子帮助',
+ 'Start' => '开始',
+ 'End' => '结束',
+ 'Task age in days' => '任务存在天数',
+ 'Days in this column' => '在此栏目的天数',
+ '%dd' => '%d天',
+ 'Add a link' => '添加一个关联',
+ 'Add a new link' => '添加一个新关联',
+ 'Do you really want to remove this link: "%s"?' => '确认要删除此关联吗:"%s"?',
+ 'Do you really want to remove this link with task #%d?' => '确认要删除到任务 #%d 的关联吗?',
+ 'Field required' => '必须的字段',
+ 'Link added successfully.' => '成功添加关联。',
+ 'Link updated successfully.' => '成功更新关联。',
+ 'Link removed successfully.' => '成功删除关联。',
+ 'Link labels' => '关联标签',
+ 'Link modification' => '关联修改',
+ 'Links' => '关联',
+ 'Link settings' => '关联设置',
+ 'Opposite label' => '反向标签',
+ 'Remove a link' => '删除关联',
+ 'Task\'s links' => '任务的关联',
+ 'The labels must be different' => '标签不能一样',
+ 'There is no link.' => '没有关联',
+ 'This label must be unique' => '关联必须唯一',
+ 'Unable to create your link.' => '无法创建关联。',
+ 'Unable to update your link.' => '无法更新关联。',
+ 'Unable to remove this link.' => '无法删除关联。',
+ 'relates to' => '关联到',
+ // 'blocks' => '',
+ // 'is blocked by' => '',
+ // 'duplicates' => '',
+ // 'is duplicated by' => '',
+ // 'is a child of' => '',
+ // 'is a parent of' => '',
+ // 'targets milestone' => '',
+ // 'is a milestone of' => '',
+ // 'fixes' => '',
+ // 'is fixed by' => '',
+ 'This task' => '此任务',
+ '<1h' => '<1h',
+ '%dh' => '%h',
+ // '%b %e' => '',
+ 'Expand tasks' => '展开任务',
+ 'Collapse tasks' => '收缩任务',
+ 'Expand/collapse tasks' => '展开/收缩任务',
+ 'Close dialog box' => '关闭对话框',
+ 'Submit a form' => '提交表单',
+ 'Board view' => '面板视图',
+ 'Keyboard shortcuts' => '键盘快捷方式',
+ 'Open board switcher' => '打开面板切换器',
+ 'Application' => '应用程序',
+ 'Filter recently updated' => '过滤最近的更新',
+ // 'since %B %e, %Y at %k:%M %p' => '',
+ 'More filters' => '更多过滤',
+ 'Compact view' => '紧凑视图',
+ 'Horizontal scrolling' => '水平滚动',
+ 'Compact/wide view' => '紧凑/宽视图',
+ 'No results match:' => '无匹配结果:',
+ 'Remove hourly rate' => '删除小时工资',
+ 'Do you really want to remove this hourly rate?' => '确定要删除此计时工资吗?',
+ 'Hourly rates' => '小时工资',
+ 'Hourly rate' => '小时工资',
+ 'Currency' => '货币',
+ 'Effective date' => '开始时间',
+ 'Add new rate' => '添加小时工资',
+ 'Rate removed successfully.' => '成功删除工资。',
+ 'Unable to remove this rate.' => '无法删除此小时工资。',
+ 'Unable to save the hourly rate.' => '无法删除小时工资。',
+ 'Hourly rate created successfully.' => '成功创建小时工资。',
+ 'Start time' => '开始时间',
+ 'End time' => '结束时1间',
+ 'Comment' => '注释',
+ 'All day' => '全天',
+ 'Day' => '日期',
+ 'Manage timetable' => '管理时间表',
+ // 'Overtime timetable' => '',
+ 'Time off timetable' => '加班时间表',
+ 'Timetable' => '时间表',
+ 'Work timetable' => '工作时间表',
+ 'Week timetable' => '周时间表',
+ 'Day timetable' => '日时间表',
+ 'From' => '从',
+ 'To' => '到',
+ 'Time slot created successfully.' => '成功创建时间段。',
+ 'Unable to save this time slot.' => '无法保存此时间段。',
+ 'Time slot removed successfully.' => '成功删除时间段。',
+ 'Unable to remove this time slot.' => '无法删除此时间段。',
+ 'Do you really want to remove this time slot?' => '确认要删除此时间段吗?',
+ 'Remove time slot' => '删除时间段',
+ 'Add new time slot' => '添加新时间段',
+ 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '如果在放假和加班计划中选择全天,则会使用这里配置的时间段。',
+ 'Files' => '文件',
+ 'Images' => '图片',
+ 'Private project' => '私人项目',
+ 'Amount' => '数量',
+ // 'AUD - Australian Dollar' => '',
+ 'Budget' => '预算',
+ 'Budget line' => '预算线',
+ 'Budget line removed successfully.' => '成功删除预算线',
+ 'Budget lines' => '预算线',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ 'Cost' => '成本',
+ 'Cost breakdown' => '成本分解',
+ 'Custom Stylesheet' => '自定义样式表',
+ 'download' => '下载',
+ 'Do you really want to remove this budget line?' => '确定要删除此预算线吗?',
+ // 'EUR - Euro' => '',
+ 'Expenses' => '花费',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ 'New budget line' => '新预算线',
+ // 'NZD - New Zealand Dollar' => '',
+ 'Remove a budget line' => '删除预算线',
+ 'Remove budget line' => '删除预算线',
+ // 'RSD - Serbian dinar' => '',
+ 'The budget line have been created successfully.' => '成功创建预算线。',
+ 'Unable to create the budget line.' => '无法创建预算线。',
+ 'Unable to remove this budget line.' => '无法删除此预算线。',
+ // 'USD - US Dollar' => '',
+ 'Remaining' => '剩余',
+ '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' => '原栏目',
+ // 'Show subtask estimates (forecast of future work)' => '',
+ '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' => '添加新汇率',
+ 'Currency rates are used to calculate project budget.' => '汇率会用来计算项目预算。',
+ 'Reference currency' => '参考货币',
+ 'The currency rate have been added successfully.' => '成功添加汇率。',
+ 'Unable to add this currency rate.' => '无法添加此汇率',
+ 'Send notifications to a Slack channel' => '发送通知到 Slack 频道',
+ 'Webhook URL' => '网络钩子 URL',
+ 'Help on Slack integration' => 'Slack 整合帮助',
+ '%s remove the assignee of the task %s' => '%s删除了任务%s的负责人',
+ 'Send notifications to Hipchat' => '发送通知到 Hipchat',
+ 'API URL' => 'API URL',
+ 'Room API ID or name' => '房间 API ID 或名称',
+ 'Room notification token' => '房间通知令牌',
+ 'Help on Hipchat integration' => 'Hipchat 整合帮助',
+ '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' => '双重认证',
+ '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' => '',
+ // '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' => '',
+ // 'Postmark (incoming emails)' => '',
+ // 'Help on Postmark integration' => '',
+ // 'Mailgun (incoming emails)' => '',
+ // 'Help on Mailgun integration' => '',
+ // 'Sendgrid (incoming emails)' => '',
+ // 'Help on Sendgrid integration' => '',
+ // '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)' => '',
+ // 'Jabber (XMPP)' => '',
+ // 'Send notifications to Jabber' => '',
+ // 'XMPP server address' => '',
+ // 'Jabber domain' => '',
+ // 'Jabber nickname' => '',
+ // 'Multi-user chat room' => '',
+ // 'Help on Jabber integration' => '',
+ // 'The server address must use this format: "tcp://hostname:5222"' => '',
+ // '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.' => '',
);
diff --git a/app/Locales/fi_FI/translations.php b/app/Locales/fi_FI/translations.php
deleted file mode 100644
index 4811b832..00000000
--- a/app/Locales/fi_FI/translations.php
+++ /dev/null
@@ -1,554 +0,0 @@
-<?php
-
-return array(
- 'None' => 'Ei mikään',
- 'edit' => 'muokkaa',
- 'Edit' => 'Muokkaa',
- 'remove' => 'poista',
- 'Remove' => 'Poista',
- 'Update' => 'Päivitä',
- 'Yes' => 'Kyllä',
- 'No' => 'Ei',
- 'cancel' => 'peruuta',
- 'or' => 'tai',
- 'Yellow' => 'Keltainen',
- 'Blue' => 'Sininen',
- 'Green' => 'Vihreä',
- 'Purple' => 'Violetti',
- 'Red' => 'Punainen',
- 'Orange' => 'Oranssi',
- 'Grey' => 'Harmaa',
- 'Save' => 'Tallenna',
- 'Login' => 'Sisäänkirjautuminen',
- 'Official website:' => 'Virallinen verkkosivu:',
- 'Unassigned' => 'Ei suorittajaa',
- 'View this task' => 'Näytä tämä tehtävä',
- 'Remove user' => 'Poista käyttäjä',
- 'Do you really want to remove this user: "%s"?' => 'Oletko varma että haluat poistaa käyttäjän "%s"?',
- 'New user' => 'Uusi käyttäjä',
- 'All users' => 'Kaikki käyttäjät',
- 'Username' => 'Käyttäjänimi',
- 'Password' => 'Salasana',
- 'Default project' => 'Oletusprojekti',
- 'Administrator' => 'Ylläpitäjä',
- 'Sign in' => 'Kirjaudu sisään',
- 'Users' => 'Käyttäjät',
- 'No user' => 'Ei käyttäjää',
- 'Forbidden' => 'Estetty',
- 'Access Forbidden' => 'Pääsy estetty',
- 'Only administrators can access to this page.' => 'Vain ylläpitäjillä on pääsy tälle sivulle.',
- 'Edit user' => 'Muokkaa käyttäjää',
- 'Logout' => 'Kirjaudu ulos',
- 'Bad username or password' => 'Väärä käyttäjätunnus tai salasana',
- 'users' => 'käyttäjät',
- 'projects' => 'projektit',
- 'Edit project' => 'Muokkaa projektia',
- 'Name' => 'Nimi',
- 'Activated' => 'Aktivoitu',
- 'Projects' => 'Projektit',
- 'No project' => 'Ei projektia',
- 'Project' => 'Projekti',
- 'Status' => 'Status',
- 'Tasks' => 'Tehtävät',
- 'Board' => 'Taulu',
- 'Actions' => 'Toiminnot',
- 'Inactive' => 'Ei aktiivinen',
- 'Active' => 'Aktiivinen',
- 'Column %d' => 'Sarake %d',
- 'Add this column' => 'Lisää tämä sarake',
- '%d tasks on the board' => '%d tehtävää taululla',
- '%d tasks in total' => '%d tehtävää yhteensä',
- 'Unable to update this board.' => 'Taulun muuttaminen ei onnistunut.',
- 'Edit board' => 'Muuta taulua',
- 'Disable' => 'Disabloi',
- 'Enable' => 'Aktivoi',
- 'New project' => 'Uusi projekti',
- 'Do you really want to remove this project: "%s"?' => 'Haluatko varmasti poistaa projektin: "%s"?',
- 'Remove project' => 'Poista projekti',
- 'Boards' => 'Taulut',
- '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',
- 'Add Column' => 'Lisää sarake',
- 'Project "%s"' => 'Projekti "%s"',
- 'Nobody assigned' => 'Ei suorittajaa',
- 'Assigned to %s' => 'Tekijä: %s',
- 'Remove a column' => 'Poista sarake',
- 'Remove a column from a board' => 'Poista sarake taulusta',
- 'Unable to remove this column.' => 'Sarakkeen poistaminen ei onnistunut.',
- 'Do you really want to remove this column: "%s"?' => 'Haluatko varmasti poistaa sarakkeen "%s"?',
- 'This action will REMOVE ALL TASKS associated to this column!' => 'Tämä toiminto POISTAA KAIKKI TEHTÄVÄT tästä sarakkeesta!',
- 'Settings' => 'Asetukset',
- 'Application settings' => 'Ohjelman asetukset',
- 'Language' => 'Kieli',
- 'Webhook token:' => 'Webhooks avain:',
- // 'API token:' => '',
- 'More information' => 'Lisätietoja',
- 'Database size:' => 'Tietokannan koko:',
- 'Download the database' => 'Lataa tietokanta',
- 'Optimize the database' => 'Optimoi tietokanta',
- '(VACUUM command)' => '(VACUUM-komento)',
- '(Gzip compressed Sqlite file)' => '(Gzip-pakattu Sqlite-tiedosto)',
- 'User settings' => 'Käyttäjän asetukset',
- 'My default project:' => 'Oletusprojektini: ',
- 'Close a task' => 'Sulje tehtävä',
- 'Do you really want to close this task: "%s"?' => 'Haluatko varmasti sulkea tehtävän: "%s"?',
- 'Edit a task' => 'Muokkaa tehtävää',
- 'Column' => 'Sarake',
- 'Color' => 'Väri',
- 'Assignee' => 'Suorittaja',
- 'Create another task' => 'Luo toinen tehtävä',
- 'New task' => 'Uusi tehtävä',
- '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.',
- 'Add a new task' => 'Lisää uusi tehtävä',
- 'The username is required' => 'Käyttäjätunnut vaaditaan',
- 'The maximum length is %d characters' => 'Maksimipituus on %d merkkiä',
- 'The minimum length is %d characters' => 'Vähimmäispituus on %d merkkiä',
- 'The password is required' => 'Salasana vaaditaan',
- 'This value must be an integer' => 'Tämän arvon täytyy olla numero',
- 'The username must be unique' => 'Käyttäjänimi täytyy olla uniikki',
- 'The username must be alphanumeric' => 'Käyttäjänimen täytyy olla alfanumeerinen',
- 'The user id is required' => 'Käyttäjän id on pakollinen',
- // 'Passwords don\'t match' => '',
- 'The confirmation is required' => 'Varmistus vaaditaan',
- 'The column is required' => 'Sarake on pakollinen',
- 'The project is required' => 'Projekti on pakollinen',
- 'The color is required' => 'Väri on pakollinen',
- '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',
- 'The language is required' => 'Kieli on pakollinen',
- 'There is no active project, the first step is to create a new project.' => 'Aktiivista projektia ei ole, ensimmäinen vaihe on luoda uusi projekti.',
- 'Settings saved successfully.' => 'Asetukset tallennettu onnistuneesti.',
- 'Unable to save your settings.' => 'Asetusten tallentaminen epäonnistui.',
- 'Database optimization done.' => 'Tietokannan optimointi suoritettu.',
- 'Your project have been created successfully.' => 'Projekti luotiin onnistuneesti.',
- 'Unable to create your project.' => 'Projektin luominen epäonnistui.',
- 'Project updated successfully.' => 'Projekti päivitettiin onnistuneesti.',
- 'Unable to update this project.' => 'Projektin muuttaminen epäonnistui.',
- 'Unable to remove this project.' => 'Projektin poistaminen epäonnistui.',
- 'Project removed successfully.' => 'Projekti poistettiin onnistuneesti.',
- 'Project activated successfully.' => 'Projekti aktivoitiin onnistuneesti.',
- 'Unable to activate this project.' => 'Projektin aktivoiminen epäonnistui.',
- 'Project disabled successfully.' => 'Projektin disabloiminen onnistui.',
- 'Unable to disable this project.' => 'Projektin disabloiminen epäonnistui.',
- 'Unable to open this task.' => 'Tehtävän avaus epäonnistui.',
- 'Task opened successfully.' => 'Tehtävä avattiin onnistuneesti.',
- 'Unable to close this task.' => 'Tehtävän sulkeminen epäonnistui.',
- 'Task closed successfully.' => 'Tehtävä suljettiin onnistuneesti.',
- 'Unable to update your task.' => 'Tehtävän muokkaaminen epäonnistui.',
- 'Task updated successfully.' => 'Tehtävä päivitettiin onnistuneesti.',
- 'Unable to create your task.' => 'Tehtävän luominen epäonnistui.',
- 'Task created successfully.' => 'Tehtävä luotiin onnistuneesti.',
- 'User created successfully.' => 'Käyttäjä lisättiin onnistuneesti.',
- 'Unable to create your user.' => 'Käyttäjän lisäys epäonnistui.',
- 'User updated successfully.' => 'Käyttäjätietojen päivitys onnistui.',
- 'Unable to update your user.' => 'Käyttäjätietojen päivitys epäonnistui.',
- 'User removed successfully.' => 'Käyttäjä poistettiin onnistuneesti.',
- 'Unable to remove this user.' => 'Käyttäjän poistaminen epäonnistui.',
- 'Board updated successfully.' => 'Taulu päivitettiin onnistuneesti.',
- 'Ready' => 'Valmis',
- 'Backlog' => 'Tehtäväjono',
- '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',
- 'No task' => 'Ei tehtävää',
- 'Completed tasks' => 'Valmiit tehtävät',
- 'List of projects' => 'Projektit',
- 'Completed tasks for "%s"' => 'Suoritetut tehtävät projektille %s',
- '%d closed tasks' => '%d suljettua tehtävää',
- 'No task for this project' => 'Ei tehtävää tälle projektille',
- 'Public link' => 'Julkinen linkki',
- 'There is no column in your project!' => 'Projektilta puuttuu sarakkeet!',
- 'Change assignee' => 'Vaihda suorittajaa',
- 'Change assignee for the task "%s"' => 'Vaihda suorittajaa tehtävälle %s',
- 'Timezone' => 'Aikavyöhyke',
- 'Sorry, I didn\'t found this information in my database!' => 'Anteeksi, en löytänyt tätä tietoa tietokannastani',
- 'Page not found' => 'Sivua ei löydy',
- 'Complexity' => 'Monimutkaisuus',
- 'limit' => 'raja',
- 'Task limit' => 'Tehtävien maksimimäärä',
- 'This value must be greater than %d' => 'Arvon täytyy olla suurempi kuin %d',
- 'Edit project access list' => 'Muuta projektin käyttäjiä',
- 'Edit users access' => 'Muuta käyttäjien pääsyä',
- 'Allow this user' => 'Salli tämä projekti',
- 'Only those users have access to this project:' => 'Vain näillä käyttäjillä on pääsy projektiin:',
- '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.' => '',
- 'You are not allowed to access to this project.' => 'Sinulla ei ole pääsyä tähän projektiin.',
- 'Comments' => 'Kommentit',
- 'Post comment' => 'Lisää kommentti',
- 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla',
- 'Leave a comment' => 'Lisää kommentti',
- 'Comment is required' => 'Kommentti vaaditaan',
- 'Leave a description' => 'Lisää kuvaus',
- 'Comment added successfully.' => 'Kommentti lisättiin onnistuneesti.',
- 'Unable to create your comment.' => 'Kommentin lisäys epäonnistui.',
- 'The description is required' => 'Kuvaus vaaditaan',
- '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',
- '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.',
- 'Remove an action' => 'Poista toiminto',
- 'Unable to remove this action.' => 'Toiminnon poistaminen epäonnistui.',
- 'Action removed successfully.' => 'Toiminto poistettiin onnistuneesti.',
- 'Automatic actions for the project "%s"' => 'Automaattiset toiminnot projektille "%s"',
- 'Defined actions' => 'Määritellyt toiminnot',
- // 'Add an action' => '',
- 'Event name' => 'Tapahtuman nimi',
- 'Action name' => 'Toiminnon nimi',
- 'Action parameters' => 'Toiminnon parametrit',
- 'Action' => 'Toiminto',
- 'Event' => 'Tapahtuma',
- 'When the selected event occurs execute the corresponding action.' => 'Kun valittu tapahtuma tapahtuu, suorita vastaava toiminto.',
- 'Next step' => 'Seuraava vaihe',
- 'Define action parameters' => 'Määrittele toiminnon parametrit',
- 'Save this action' => 'Tallenna toiminto',
- 'Do you really want to remove this action: "%s"?' => 'Oletko varma että haluat poistaa toiminnon "%s"?',
- 'Remove an automatic action' => 'Poista automaattintn toiminto',
- 'Close the task' => 'Sulje tehtävä',
- 'Assign the task to a specific user' => 'Osoita tehtävä käyttäjälle',
- 'Assign the task to the person who does the action' => 'Määritä suorittaja tehtävälle',
- 'Duplicate the task to another project' => 'Monista tehtävä toiselle projektille',
- 'Move a task to another column' => 'Siirrä tehtävä toiseen sarakkeeseen',
- 'Move a task to another position in the same column' => 'Siirrä tehtävä eri järjestykseen samassa sarakkeessa',
- 'Task modification' => 'Tehtävän muokkaus',
- 'Task creation' => 'Tehtävän luominen',
- 'Open a closed task' => 'Avaa jo suljettu tehtävä',
- 'Closing a task' => 'Tehtävää suljetaan',
- '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',
- 'Update this comment' => 'Muuta projektia',
- 'Comment updated successfully.' => 'Kommentti päivitettiin onnistuneesti.',
- 'Unable to update your comment.' => 'Kommentin päivitys epäonnistui.',
- 'Remove a comment' => 'Poista kommentti',
- '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.',
- 'Details' => 'Tiedot',
- 'Current password for the user "%s"' => 'Käyttäjän "%s" salasana',
- 'The current password is required' => 'Salasana vaaditaan',
- 'Wrong password' => 'Väärä salasana',
- 'Reset all tokens' => 'Resetoi kaikki tokenit',
- 'All tokens have been regenerated.' => 'Kaikki tokenit luotiin uudelleen.',
- 'Unknown' => 'Tuntematon',
- 'Last logins' => 'Viimeisimmät kirjautumiset',
- 'Login date' => 'Kirjautumispäivä',
- 'Authentication method' => 'Autentikointimenetelmä',
- 'IP address' => 'IP-Osoite',
- 'User agent' => 'Selain',
- 'Persistent connections' => 'Voimassa olevat yhteydet',
- 'No session.' => 'Ei sessioita.',
- 'Expiration date' => 'Vanhentumispäivä',
- 'Remember Me' => 'Muista minut',
- 'Creation date' => 'Luomispäivä',
- 'Filter by user' => 'Rajaa käyttäjän mukaan',
- 'Filter by due date' => 'Rajaa deadlinen mukaan',
- 'Everybody' => 'Kaikki',
- 'Open' => 'Avoin',
- 'Closed' => 'Suljettu',
- 'Search' => 'Etsi',
- 'Nothing found.' => 'Ei löytynyt.',
- 'Search in the project "%s"' => 'Etsi projektista "%s"',
- 'Due date' => 'Deadline',
- 'Others formats accepted: %s and %s' => 'Muut hyväksytyt muodot: %s ja %s',
- 'Description' => 'Kuvaus',
- '%d comments' => '%d kommenttia',
- '%d comment' => '%d kommentti',
- 'Email address invalid' => 'Email ei kelpaa',
- 'Your Google Account is not linked anymore to your profile.' => 'Google tunnustasi ei ole enää linkattu profiiliisi',
- 'Unable to unlink your Google Account.' => 'Google tunnuksen linkkaamisen poistaminen epäonnistui.',
- 'Google authentication failed' => 'Google autentikointi epäonnistui',
- 'Unable to link your Google Account.' => 'Google tunnuksen linkkaaminen epäonnistui.',
- 'Your Google Account is linked to your profile successfully.' => 'Google tunnuksesi linkitettiin profiiliisi onnistuneesti.',
- '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 #%d' => 'Tehtävä #%d',
- 'Task removed successfully.' => 'Tehtävä poistettiin onnistuneesti.',
- 'Unable to remove this task.' => 'Tehtävän poistaminen epäonnistui.',
- 'Remove a task' => 'Poista tehtävä',
- 'Do you really want to remove this task: "%s"?' => 'Haluatko varmasti poistaa tehtävän: "%s"?',
- 'Assign automatically a color based on a category' => 'Aseta väri automaattisesti kategorian mukaan',
- 'Assign automatically a category based on a color' => 'Aseta kategoria automaattisesti värin mukaan',
- 'Task creation or modification' => 'Tehtävän luonti tai muuttaminen',
- 'Category' => 'Kategoria',
- 'Category:' => 'Kategoria:',
- 'Categories' => 'Kategoriat',
- 'Category not found.' => 'Kategoriaa ei löytynyt.',
- 'Your category have been created successfully.' => 'Kategoria luotiin onnistuneesti.',
- 'Unable to create your category.' => 'Kategorian luonti epäonnistui.',
- 'Your category have been updated successfully.' => 'Kategoriaa muokattiin onnistuneesti.',
- 'Unable to update your category.' => 'Kategorian muokkaaminen epäonnistui.',
- 'Remove a category' => 'Poista kategoria',
- 'Category removed successfully.' => 'Kategoria poistettu onnistuneesti.',
- 'Unable to remove this category.' => 'Kategorian poisto epäonnistui.',
- 'Category modification for the project "%s"' => 'Kategorian muutos projektissa "%s"',
- 'Category Name' => 'Kategorian nimi',
- 'Categories for the project "%s"' => 'Kategoriat projektille "%s"',
- 'Add a new category' => 'Lisää uusi kategoria',
- 'Do you really want to remove this category: "%s"?' => 'Haluatko varmasti poistaa kategorian: "%s"?',
- 'Filter by category' => 'Rajaa kategorian mukaan',
- 'All categories' => 'Kaikki kategoriat',
- 'No category' => 'Kategoriaa ei löydy',
- 'The name is required' => 'Nimi vaaditaan',
- 'Remove a file' => 'Poista tiedosto',
- 'Unable to remove this file.' => 'Tiedoston poistaminen epäonnistui.',
- 'File removed successfully.' => 'Tiedosto poistettiin onnistuneesti.',
- 'Attach a document' => 'Liitä dokumentti',
- 'Do you really want to remove this file: "%s"?' => 'Haluatko varmasti poistaa tiedoston: "%s"?',
- 'open' => 'avaa',
- 'Attachments' => 'Liitteet',
- 'Edit the task' => 'Muokkaa tehtävää',
- 'Edit the description' => 'Muokkaa kuvausta',
- 'Add a comment' => 'Lisää kommentti',
- 'Edit a comment' => 'Muokkaa kommenttia',
- 'Summary' => 'Yhteenveto',
- 'Time tracking' => 'Ajan seuranta',
- 'Estimate:' => 'Arvio:',
- 'Spent:' => 'Käytetty:',
- 'Do you really want to remove this sub-task?' => 'Haluatko varmasti poistaa tämän alitehtävän?',
- 'Remaining:' => 'Jäljellä',
- 'hours' => 'tuntia',
- 'spent' => 'käytetty',
- 'estimated' => 'estimoitu',
- 'Sub-Tasks' => 'Alitehtävät',
- 'Add a sub-task' => 'Lisää alitehtävä',
- 'Original estimate' => 'Alkuperäinen estimaatti',
- 'Create another sub-task' => 'Lisää toinen alitehtävä',
- 'Time spent' => 'Käytetty aika',
- 'Edit a sub-task' => 'Muokkaa alitehtävää',
- 'Remove a sub-task' => 'Poista alitehtävä',
- 'The time must be a numeric value' => 'Ajan pitää olla numero',
- 'Todo' => 'Todo',
- 'In progress' => 'Työnalla',
- 'Sub-task removed successfully.' => 'Alitehtävä poistettu onnistuneesti.',
- 'Unable to remove this sub-task.' => 'Alitehtävän poistaminen epäonnistui.',
- 'Sub-task updated successfully.' => 'Alitehtävä päivitettiin onnistuneesti.',
- 'Unable to update your sub-task.' => 'Alitehtävän päivitys epäonnistui.',
- 'Unable to create your sub-task.' => 'Alitehtävän luonti epäonnistui.',
- 'Sub-task added successfully.' => 'Alitehtävä luotiin onnistuneesti.',
- 'Maximum size: ' => 'Maksimikoko: ',
- 'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.',
- 'Display another project' => 'Näytä toinen projekti',
- // 'Your GitHub account was successfully linked to your profile.' => '',
- // 'Unable to link your GitHub Account.' => '',
- // 'GitHub authentication failed' => '',
- // 'Your GitHub account is no longer linked to your profile.' => '',
- // 'Unable to unlink your GitHub Account.' => '',
- // 'Login with my GitHub Account' => '',
- // 'Link my GitHub Account' => '',
- // 'Unlink my GitHub Account' => '',
- '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ä',
- 'End Date' => 'Lopetuspäivä',
- 'Execute' => 'Suorita',
- 'Task Id' => 'Tehtävän ID',
- 'Creator' => 'Luonut',
- 'Modification date' => 'Muokkauspäivä',
- 'Completion date' => 'Valmistumispäivä',
- 'Webhook URL for task creation' => 'Webhook URL tehtävän luomiselle',
- 'Webhook URL for task modification' => 'Webhook URL tehtävän muokkaamiselle',
- // 'Clone' => '',
- // 'Clone Project' => '',
- // 'Project cloned successfully.' => '',
- // 'Unable to clone this project.' => '',
- // 'Email notifications' => '',
- // 'Enable email notifications' => '',
- // 'Task position:' => '',
- // 'The task #%d have been opened.' => '',
- // 'The task #%d have been closed.' => '',
- // 'Sub-task updated' => '',
- // 'Title:' => '',
- // 'Status:' => '',
- // 'Assignee:' => '',
- // 'Time tracking:' => '',
- // 'New sub-task' => '',
- // 'New attachment added "%s"' => '',
- // 'Comment updated' => '',
- // 'New comment posted by %s' => '',
- // 'List of due tasks for the project "%s"' => '',
- // '[%s][New attachment] %s (#%d)' => '',
- // '[%s][New comment] %s (#%d)' => '',
- // '[%s][Comment updated] %s (#%d)' => '',
- // '[%s][New subtask] %s (#%d)' => '',
- // '[%s][Subtask updated] %s (#%d)' => '',
- // '[%s][New task] %s (#%d)' => '',
- // '[%s][Task updated] %s (#%d)' => '',
- // '[%s][Task closed] %s (#%d)' => '',
- // '[%s][Task opened] %s (#%d)' => '',
- // '[%s][Due tasks]' => '',
- // '[Kanboard] Notification' => '',
- // 'I want to receive notifications only for those projects:' => '',
- // 'view the task on Kanboard' => '',
- // 'Public access' => '',
- // 'Category management' => '',
- // 'User management' => '',
- // 'Active tasks' => '',
- // 'Disable public access' => '',
- // 'Enable public access' => '',
- // 'Active projects' => '',
- // 'Inactive projects' => '',
- // 'Public access disabled' => '',
- // 'Do you really want to disable this project: "%s"?' => '',
- // 'Do you really want to duplicate this project: "%s"?' => '',
- // 'Do you really want to enable this project: "%s"?' => '',
- // 'Project activation' => '',
- // 'Move the task to another project' => '',
- // 'Move to another project' => '',
- // 'Do you really want to duplicate this task?' => '',
- // 'Duplicate a task' => '',
- // 'External accounts' => '',
- // 'Account type' => '',
- // 'Local' => '',
- // 'Remote' => '',
- // 'Enabled' => '',
- // 'Disabled' => '',
- // 'Google account linked' => '',
- // 'Github account linked' => '',
- // 'Username:' => '',
- // 'Name:' => '',
- // 'Email:' => '',
- // 'Default project:' => '',
- // 'Notifications:' => '',
- // 'Notifications' => '',
- // 'Group:' => '',
- // 'Regular user' => '',
- // '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"' => '',
- // 'Change category' => '',
- // '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
- // '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
- // '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // 'Assigned to %s with an estimate of %s/%sh' => '',
- // 'Not assigned, estimate of %sh' => '',
- // '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s\'s activity' => '',
- // 'No activity.' => '',
- // 'RSS feed' => '',
- // '%s updated a comment on the task #%d' => '',
- // '%s commented on the task #%d' => '',
- // '%s updated a subtask for the task #%d' => '',
- // '%s created a subtask for the task #%d' => '',
- // '%s updated the task #%d' => '',
- // '%s created the task #%d' => '',
- // '%s closed the task #%d' => '',
- // '%s open the task #%d' => '',
- // '%s moved the task #%d to the column "%s"' => '',
- // '%s moved the task #%d to the position %d in the column "%s"' => '',
- // 'Activity' => '',
- // 'Default values are "%s"' => '',
- // 'Default columns for new projects (Comma-separated)' => '',
- // 'Task assignee change' => '',
- // '%s change the assignee of the task #%d to %s' => '',
- // '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '',
- // '[%s][Column Change] %s (#%d)' => '',
- // '[%s][Position Change] %s (#%d)' => '',
- // '[%s][Assignee Change] %s (#%d)' => '',
- // 'New password for the user "%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' => '',
- // 'Label' => '',
- // 'Database' => '',
- // 'About' => '',
- // 'Database driver:' => '',
- // 'Board settings' => '',
- // 'URL and token' => '',
- // 'Webhook settings' => '',
- // 'URL for task creation:' => '',
- // 'Reset token' => '',
- // 'API endpoint:' => '',
- // 'Refresh interval for private board' => '',
- // 'Refresh interval for public board' => '',
- // 'Task highlight period' => '',
- // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
- // 'Frequency in second (60 seconds by default)' => '',
- // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
- // 'Application URL' => '',
- // 'Example: http://example.kanboard.net/ (used by email notifications)' => '',
- // 'Token regenerated.' => '',
- // 'Date format' => '',
- // 'ISO format is always accepted, example: "%s" and "%s"' => '',
- // 'New private project' => '',
- // 'This project is private' => '',
- // 'Type here to create a new sub-task' => '',
- // 'Add' => '',
- // 'Estimated time: %s hours' => '',
- // 'Time spent: %s hours' => '',
- // 'Started on %B %e, %Y' => '',
- // '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.' => '',
-);
diff --git a/app/Locales/it_IT/translations.php b/app/Locales/it_IT/translations.php
deleted file mode 100644
index 4d2cfc91..00000000
--- a/app/Locales/it_IT/translations.php
+++ /dev/null
@@ -1,554 +0,0 @@
-<?php
-
-return array(
- 'None' => 'Nessuno',
- 'edit' => 'modificare',
- 'Edit' => 'Modificare',
- 'remove' => 'cancellare',
- 'Remove' => 'Cancellare',
- 'Update' => 'Aggiornare',
- 'Yes' => 'Si',
- 'No' => 'No',
- 'cancel' => 'annullare',
- 'or' => 'o',
- 'Yellow' => 'Giallo',
- 'Blue' => 'Blu',
- 'Green' => 'Verde',
- 'Purple' => 'Viola',
- 'Red' => 'Rosso',
- 'Orange' => 'Arancione',
- 'Grey' => 'Grigio',
- 'Save' => 'Salvare',
- 'Login' => 'Entra',
- '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',
- 'All users' => 'Tutti gli utenti',
- 'Username' => 'Nome utente',
- 'Password' => 'Password',
- 'Default project' => 'Progetto predefinito',
- 'Administrator' => 'Amministratore',
- 'Sign in' => 'Iscriversi',
- 'Users' => 'Utenti',
- 'No user' => 'Nessun utente',
- 'Forbidden' => 'Vietato',
- 'Access Forbidden' => 'Accesso vietato',
- 'Only administrators can access to this page.' => 'Solo gli amministratori possono accedere a questa pagina.',
- 'Edit user' => 'Modificare un utente',
- 'Logout' => 'Uscire',
- 'Bad username or password' => 'Utente o password errati',
- 'users' => 'utenti',
- 'projects' => 'progetti',
- 'Edit project' => 'Modificare progetto',
- 'Name' => 'Nome',
- 'Activated' => 'Attivo',
- 'Projects' => 'Progetti',
- 'No project' => 'Nessun progetto',
- 'Project' => 'Progetto',
- 'Status' => 'Stato',
- 'Tasks' => 'Compiti',
- 'Board' => 'Bacheca',
- // 'Actions' => '',
- 'Inactive' => 'Inattivo',
- 'Active' => 'Attivo',
- 'Column %d' => 'Colonna %d',
- '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',
- 'New project' => 'Nuovo progetto',
- 'Do you really want to remove this project: "%s"?' => 'Veramente vuoi eliminare questo progetto: « %s » ?',
- 'Remove project' => 'Cancellare il progetto',
- 'Boards' => 'Bacheche',
- 'Edit the board for "%s"' => 'Modificare la bacheca per « %s »',
- 'All projects' => 'Tutti i progetti',
- 'Change columns' => 'Cambiare le colonne',
- 'Add a new column' => 'Aggiungere una nuova colonna',
- 'Title' => 'Titolo',
- 'Add Column' => 'Aggiungere colonna',
- 'Project "%s"' => 'progetto « %s »',
- '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!',
- 'Settings' => 'Impostazioni',
- 'Application settings' => 'Impostazioni dell\'applicazione',
- 'Language' => 'Lingua',
- 'Webhook token:' => 'Identificatore (token) per i webhooks :',
- // 'API token:' => '',
- 'More information' => 'Più informazioni',
- 'Database size:' => 'Dimensioni della base dati:',
- 'Download the database' => 'Scaricare la base dati',
- 'Optimize the database' => 'Ottimizare la base dati',
- '(VACUUM command)' => '(Comando VACUUM)',
- '(Gzip compressed Sqlite file)' => '(File Sqlite compresso in Gzip)',
- 'User settings' => 'Impostazioni di utente',
- 'My default project:' => 'Il mio progetto predefinito:',
- 'Close a task' => 'Chiudere un compito',
- 'Do you really want to close this task: "%s"?' => 'Veramente desideri chiudere questo compito: « %s » ?',
- 'Edit a task' => 'Modificare un compito',
- 'Column' => 'colonna',
- // 'Color' => '',
- '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' => '',
- 'There is nobody assigned' => 'Non c\'è nessuno assegnato a questo compito',
- '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',
- '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',
- 'The password is required' => 'Si richiede una password',
- 'This value must be an integer' => 'questo valore deve essere un intero',
- 'The username must be unique' => 'Il nome di utente deve essere unico',
- 'The username must be alphanumeric' => 'Il nome di utente deve essere alfanumerico',
- 'The user id is required' => 'Si richiede l\'identificatore dell\'utente',
- // 'Passwords don\'t match' => '',
- 'The confirmation is required' => 'Si richiede una conferma',
- 'The column is required' => 'Si richiede una colonna',
- 'The project is required' => 'Si richiede il progetto',
- 'The color is required' => 'Si richiede il colore',
- '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',
- 'The language is required' => 'Si richiede una lingua',
- 'There is no active project, the first step is to create a new project.' => 'Non ci sono progetti attivi, il primo passo consiste in creare un nuovo progetto.',
- 'Settings saved successfully.' => 'Impostazioni salvate correttamente.',
- 'Unable to save your settings.' => 'Non si possono 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.',
- '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.',
- 'Project removed successfully.' => 'Progetto cancellato correttamente.',
- 'Project activated successfully.' => 'Progetto attivato correttamente.',
- 'Unable to activate this project.' => 'Non si può 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.',
- 'User created successfully.' => 'Utente creato correttamente.',
- 'Unable to create your user.' => 'Non si può creare l\'utente.',
- 'User updated successfully.' => 'Utente aggiornato correttamente.',
- 'Unable to update your user.' => 'Non si può aggiornare questo utente.',
- 'User removed successfully.' => 'Utente cancellato correttamente.',
- 'Unable to remove this user.' => 'Non si può 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' => '',
- // '%B %e, %Y at %k:%M %p' => '',
- 'Date created' => 'Data di creazione',
- 'Date completed' => 'Data di termine',
- 'Id' => 'Identificatore',
- 'No task' => 'Nessun compito',
- 'Completed tasks' => 'Compiti fatti',
- 'List of projects' => 'Lista di progetti',
- 'Completed tasks for "%s"' => 'Compiti fatti da « %s »',
- '%d closed tasks' => '%d compiti chiusi',
- 'No task for this project' => 'Nessun compito per questo progetto',
- 'Public link' => 'Link pubblico',
- 'There is no column in your project!' => 'Non c\'è nessuna colonna per questo progetto!',
- 'Change assignee' => 'Cambiare la persona assegnata',
- 'Change assignee for the task "%s"' => 'Cambiare la persona assegnata per il compito « %s »',
- 'Timezone' => 'Fuso orario',
- 'Sorry, I didn\'t found this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!',
- 'Page not found' => 'Pagina non trovata',
- // 'Complexity' => '',
- 'limit' => 'limite',
- 'Task limit' => 'Numero massimo di compiti',
- 'This value must be greater than %d' => 'questo valore deve essere maggiore di %d',
- 'Edit project access list' => 'Modificare i permessi del progetto',
- 'Edit users access' => 'Modificare i permessi degli utenti',
- 'Allow this user' => 'Permettere a questo utente',
- 'Only those users have access to this project:' => 'Solo questi utenti hanno accesso a questo progetto:',
- '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',
- 'User' => 'Utente',
- // 'Nobody have access to this project.' => '',
- 'You are not allowed to access to this project.' => 'Non hai l\'accesso a questo progetto.',
- 'Comments' => 'Commenti',
- 'Post comment' => 'Mandare commento',
- 'Write your text in Markdown' => 'Scrivi il testo in Markdown',
- 'Leave a comment' => 'Lasciare un commento',
- 'Comment is required' => 'Si richiede un commento',
- 'Leave a description' => 'Lasciare una descrizione',
- 'Comment added successfully.' => 'Commenti aggiunti correttamente.',
- 'Unable to create your comment.' => 'Non si può creare questo commento.',
- 'The description is required' => 'Si richiede una descrizione',
- 'Edit this task' => 'Modificare questo compito',
- 'Due Date' => 'Data di scadenza',
- 'Invalid date' => 'Data sbagliata',
- // 'Must be done before %B %e, %Y' => '',
- // '%B %e, %Y' => '',
- '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.',
- 'Remove an action' => 'Cancellare un\'azione',
- 'Unable to remove this action.' => 'Non si può cancellare questa azione.',
- 'Action removed successfully.' => 'Azione cancellata correttamente.',
- 'Automatic actions for the project "%s"' => 'Azioni automatiche per questo progetto « %s »',
- 'Defined actions' => 'Azioni definite',
- // 'Add an action' => '',
- 'Event name' => 'Nome dell\'evento',
- 'Action name' => 'Nome dell\'azione',
- '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',
- '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',
- 'Close the task' => 'Chiudere questo compito',
- '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',
- 'Move a task to another position in the same column' => 'Muovere un compito in un\'altra posizione sulla stessa colonna',
- 'Task modification' => 'Modifica di un compito',
- 'Task creation' => 'Creazione di un compito',
- 'Open a closed task' => 'Riaprire un compito',
- 'Closing a task' => 'Chiudere un compito',
- // 'Assign a color to a specific user' => '',
- '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',
- 'Update this comment' => 'Aggiornare questo commento',
- 'Comment updated successfully.' => 'Commento aggiornato correttamente.',
- 'Unable to update your comment.' => 'Non si può aggiornare questo commento.',
- 'Remove a comment' => 'Cancellare 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.',
- 'Details' => 'Dettagli',
- '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',
- 'Reset all tokens' => 'Azzerare gli identificatori (tokens) di sicurezza ',
- 'All tokens have been regenerated.' => 'Tutti gli identificatori (tokens) sono stati rigenerati.',
- 'Unknown' => 'Sconociuto',
- 'Last logins' => 'Ultimi ingressi',
- 'Login date' => 'Data di ingresso',
- 'Authentication method' => 'Metodo di autenticazzione',
- 'IP address' => 'Indirizzo IP',
- 'User agent' => 'Navigatore',
- 'Persistent connections' => 'Connessioni persistenti',
- 'No session.' => 'Non esiste sessione.',
- 'Expiration date' => 'Data di scadenza',
- 'Remember Me' => 'Ricordami',
- 'Creation date' => 'Data di creazione',
- 'Filter by user' => 'Filtrato mediante utente',
- 'Filter by due date' => 'Filtrare attraverso data di scadenza',
- 'Everybody' => 'Tutti',
- 'Open' => 'Aperto',
- 'Closed' => 'Chiuso',
- 'Search' => 'Cercare',
- 'Nothing found.' => 'Non si è trovato nulla.',
- 'Search in the project "%s"' => 'Cercare nel progetto "%s"',
- 'Due date' => 'Data di scadenza',
- 'Others formats accepted: %s and %s' => 'Altri formati accettati: %s y %s',
- 'Description' => 'Descrizione',
- '%d comments' => '%d commenti',
- '%d comment' => '%d commento',
- 'Email address invalid' => 'Indirizzo e-mail sbagliato',
- 'Your Google Account is not linked anymore to your profile.' => 'Il suo account Google non è più collegato al suo profilo',
- 'Unable to unlink your Google Account.' => 'Non si può svincolare l\'account di Google.',
- 'Google authentication failed' => 'Autenticazione con Google non riuscita',
- 'Unable to link your Google Account.' => 'Non si può collegare il tuo account di Google.',
- 'Your Google Account is linked to your profile successfully.' => 'Il tuo account di Google è stato collegato correttamente al tuo profilo.',
- '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 #%d' => 'Compito numero %d',
- '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',
- '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.',
- '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',
- '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',
- 'Categories for the project "%s"' => 'Categorie per il progetto',
- 'Add a new category' => 'Aggiungere una nuova categoria',
- 'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare questa categoria: "%s"?',
- 'Filter by category' => 'Filtrare attraverso categoria',
- '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.',
- 'File removed successfully.' => 'File cancellato correttamente.',
- 'Attach a document' => 'Allegare un documento',
- 'Do you really want to remove this file: "%s"?' => 'Vuoi veramente cancellare questo file: "%s"?',
- 'open' => 'aprire',
- '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',
- 'Summary' => 'Sommario',
- 'Time tracking' => 'Time tracking',
- 'Estimate:' => 'Stimato:',
- 'Spent:' => 'Trascorso:',
- 'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sotto-compito?',
- 'Remaining:' => 'Rimangono',
- 'hours' => 'ore',
- 'spent' => 'trascorse',
- 'estimated' => 'stimate',
- 'Sub-Tasks' => 'Sotto-compiti',
- 'Add a sub-task' => 'Aggiungere un sotto-compito',
- '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',
- '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',
- 'Your GitHub account was successfully linked to your profile.' => 'Il suo account di Github è stato collegato correttamente col tuo profilo.',
- 'Unable to link your GitHub Account.' => 'Non si può collegarre il tuo account di Github.',
- 'GitHub authentication failed' => 'Autenticazione con GitHub non riuscita',
- 'Your GitHub account is no longer linked to your profile.' => 'Il tuo account di Github non è più collegato al tuo profilo.',
- 'Unable to unlink your GitHub Account.' => 'Non si può collegare il tuo account di Github.',
- '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',
- '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 »',
- 'Start Date' => 'Data d\'inizio',
- 'End Date' => 'Data di fine',
- 'Execute' => 'Eseguire',
- 'Task Id' => 'Identificatore del compito',
- 'Creator' => 'Creatore',
- 'Modification date' => 'Data di modifica',
- 'Completion date' => 'Data di termine',
- // 'Webhook URL for task creation' => '',
- // 'Webhook URL for task modification' => '',
- // 'Clone' => '',
- // 'Clone Project' => '',
- // 'Project cloned successfully.' => '',
- // 'Unable to clone this project.' => '',
- // 'Email notifications' => '',
- // 'Enable email notifications' => '',
- // 'Task position:' => '',
- // 'The task #%d have been opened.' => '',
- // 'The task #%d have been closed.' => '',
- // 'Sub-task updated' => '',
- // 'Title:' => '',
- // 'Status:' => '',
- // 'Assignee:' => '',
- // 'Time tracking:' => '',
- // 'New sub-task' => '',
- 'New attachment added "%s"' => 'Nuovo allegato aggiunto « %s »',
- 'Comment updated' => 'Commento aggiornato',
- 'New comment posted by %s' => 'Nuovo commento aggiunto da « %s »',
- 'List of due tasks for the project "%s"' => 'Lista dei compiti scaduti per il progetto « %s »',
- '[%s][New attachment] %s (#%d)' => '[%s][Nuovo allegato] %s (#%d)',
- '[%s][New comment] %s (#%d)' => '[%s][Nuovo commento] %s (#%d)',
- '[%s][Comment updated] %s (#%d)' => '[%s][Commento aggiornato] %s (#%d)',
- '[%s][New subtask] %s (#%d)' => '[%s][Nuovo sotto-compito] %s (#%d)',
- '[%s][Subtask updated] %s (#%d)' => '[%s][Sotto-compito aggiornato] %s (#%d)',
- '[%s][New task] %s (#%d)' => '[%s][Nuovo compito] %s (#%d)',
- '[%s][Task updated] %s (#%d)' => '[%s][Compito aggiornato] %s (#%d)',
- '[%s][Task closed] %s (#%d)' => '[%s][Compito chiuso] %s (#%d)',
- '[%s][Task opened] %s (#%d)' => '[%s][Compito aperto] %s (#%d)',
- '[%s][Due tasks]' => '[%s][Compiti scaduti]',
- '[Kanboard] Notification' => '[Kanboard] Notifica',
- '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',
- // 'Public access' => '',
- // 'Category management' => '',
- // 'User management' => '',
- // 'Active tasks' => '',
- // 'Disable public access' => '',
- // 'Enable public access' => '',
- // 'Active projects' => '',
- // 'Inactive projects' => '',
- // 'Public access disabled' => '',
- // 'Do you really want to disable this project: "%s"?' => '',
- // 'Do you really want to duplicate this project: "%s"?' => '',
- // 'Do you really want to enable this project: "%s"?' => '',
- // 'Project activation' => '',
- // 'Move the task to another project' => '',
- // 'Move to another project' => '',
- // 'Do you really want to duplicate this task?' => '',
- // 'Duplicate a task' => '',
- // 'External accounts' => '',
- // 'Account type' => '',
- // 'Local' => '',
- // 'Remote' => '',
- // 'Enabled' => '',
- // 'Disabled' => '',
- // 'Google account linked' => '',
- // 'Github account linked' => '',
- // 'Username:' => '',
- // 'Name:' => '',
- // 'Email:' => '',
- // 'Default project:' => '',
- // 'Notifications:' => '',
- // 'Notifications' => '',
- // 'Group:' => '',
- // 'Regular user' => '',
- // '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"' => '',
- // 'Change category' => '',
- // '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
- // '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
- // '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // 'Assigned to %s with an estimate of %s/%sh' => '',
- // 'Not assigned, estimate of %sh' => '',
- // '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s\'s activity' => '',
- // 'No activity.' => '',
- // 'RSS feed' => '',
- // '%s updated a comment on the task #%d' => '',
- // '%s commented on the task #%d' => '',
- // '%s updated a subtask for the task #%d' => '',
- // '%s created a subtask for the task #%d' => '',
- // '%s updated the task #%d' => '',
- // '%s created the task #%d' => '',
- // '%s closed the task #%d' => '',
- // '%s open the task #%d' => '',
- // '%s moved the task #%d to the column "%s"' => '',
- // '%s moved the task #%d to the position %d in the column "%s"' => '',
- // 'Activity' => '',
- // 'Default values are "%s"' => '',
- // 'Default columns for new projects (Comma-separated)' => '',
- // 'Task assignee change' => '',
- // '%s change the assignee of the task #%d to %s' => '',
- // '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '',
- // '[%s][Column Change] %s (#%d)' => '',
- // '[%s][Position Change] %s (#%d)' => '',
- // '[%s][Assignee Change] %s (#%d)' => '',
- // 'New password for the user "%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' => '',
- // 'Label' => '',
- // 'Database' => '',
- // 'About' => '',
- // 'Database driver:' => '',
- // 'Board settings' => '',
- // 'URL and token' => '',
- // 'Webhook settings' => '',
- // 'URL for task creation:' => '',
- // 'Reset token' => '',
- // 'API endpoint:' => '',
- // 'Refresh interval for private board' => '',
- // 'Refresh interval for public board' => '',
- // 'Task highlight period' => '',
- // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
- // 'Frequency in second (60 seconds by default)' => '',
- // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
- // 'Application URL' => '',
- // 'Example: http://example.kanboard.net/ (used by email notifications)' => '',
- // 'Token regenerated.' => '',
- // 'Date format' => '',
- // 'ISO format is always accepted, example: "%s" and "%s"' => '',
- // 'New private project' => '',
- // 'This project is private' => '',
- // 'Type here to create a new sub-task' => '',
- // 'Add' => '',
- // 'Estimated time: %s hours' => '',
- // 'Time spent: %s hours' => '',
- // 'Started on %B %e, %Y' => '',
- // '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.' => '',
-);
diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php
deleted file mode 100644
index 1ca201c1..00000000
--- a/app/Locales/pl_PL/translations.php
+++ /dev/null
@@ -1,554 +0,0 @@
-<?php
-
-return array(
- 'None' => 'Brak',
- 'edit' => 'edytuj',
- 'Edit' => 'Edytuj',
- 'remove' => 'usuń',
- 'Remove' => 'Usuń',
- 'Update' => 'Aktualizuj',
- 'Yes' => 'Tak',
- 'No' => 'Nie',
- 'cancel' => 'anuluj',
- 'or' => 'lub',
- 'Yellow' => 'Żółty',
- 'Blue' => 'Niebieski',
- 'Green' => 'Zielony',
- 'Purple' => 'Fioletowy',
- 'Red' => 'Czerwony',
- 'Orange' => 'Pomarańczowy',
- 'Grey' => 'Szary',
- 'Save' => 'Zapisz',
- 'Login' => 'Login',
- 'Official website:' => 'Oficjalna strona:',
- 'Unassigned' => 'Nieprzypisany',
- 'View this task' => 'Zobacz zadanie',
- 'Remove user' => 'Usuń użytkownika',
- 'Do you really want to remove this user: "%s"?' => 'Na pewno chcesz usunąć użytkownika: "%s"?',
- 'New user' => 'Nowy użytkownik',
- 'All users' => 'Wszyscy użytkownicy',
- 'Username' => 'Nazwa użytkownika',
- 'Password' => 'Hasło',
- 'Default project' => 'Domyślny projekt',
- 'Administrator' => 'Administrator',
- 'Sign in' => 'Zaloguj',
- 'Users' => 'Użytkownicy',
- 'No user' => 'Brak użytkowników',
- 'Forbidden' => 'Zabroniony',
- 'Access Forbidden' => 'Dostęp zabroniony',
- 'Only administrators can access to this page.' => 'Tylko administrator może wejść na tą stronę.',
- 'Edit user' => 'Edytuj użytkownika',
- 'Logout' => 'Wyloguj',
- 'Bad username or password' => 'Zła nazwa uyżytkownika lub hasło',
- 'users' => 'użytkownicy',
- 'projects' => 'projekty',
- 'Edit project' => 'Edytuj projekt',
- 'Name' => 'Nazwa',
- 'Activated' => 'Aktywny',
- 'Projects' => 'Projekty',
- 'No project' => 'Brak projektów',
- 'Project' => 'Projekt',
- 'Status' => 'Status',
- 'Tasks' => 'Zadania',
- 'Board' => 'Tablica',
- 'Actions' => 'Akcje',
- 'Inactive' => 'Nieaktywny',
- 'Active' => 'Aktywny',
- 'Column %d' => 'Kolumna %d',
- 'Add this column' => 'Dodaj kolumnę',
- '%d tasks on the board' => '%d zadań na tablicy',
- '%d tasks in total' => '%d wszystkich zadań',
- 'Unable to update this board.' => 'Nie można zaktualizować tablicy.',
- 'Edit board' => 'Edytuj tablicę',
- 'Disable' => 'Wyłącz',
- 'Enable' => 'Włącz',
- 'New project' => 'Nowy projekt',
- 'Do you really want to remove this project: "%s"?' => 'Na pewno chcesz usunąć projekt: "%s"?',
- 'Remove project' => 'Usuń projekt',
- 'Boards' => 'Tablice',
- 'Edit the board for "%s"' => 'Edytuj tablię dla "%s"',
- 'All projects' => 'Wszystkie projekty',
- 'Change columns' => 'Zmień kolumny',
- 'Add a new column' => 'Dodaj nową kolumnę',
- 'Title' => 'Tytuł',
- 'Add Column' => 'Dodaj kolumnę',
- 'Project "%s"' => 'Projekt "%s"',
- 'Nobody assigned' => 'Nikt nie przypisany',
- 'Assigned to %s' => 'Przypisane do %s',
- 'Remove a column' => 'Usuń kolumnę',
- 'Remove a column from a board' => 'Usuń kolumnę z tablicy',
- 'Unable to remove this column.' => 'Nie udało się usunąć kolumny.',
- 'Do you really want to remove this column: "%s"?' => 'Na pewno chcesz usunąć kolumnę: "%s"?',
- 'This action will REMOVE ALL TASKS associated to this column!' => 'Wszystkie zadania w kolumnie zostaną usunięte!',
- 'Settings' => 'Ustawienia',
- 'Application settings' => 'Ustawienia aplikacji',
- 'Language' => 'Język',
- 'Webhook token:' => 'Token :',
- // 'API token:' => '',
- 'More information' => 'Więcej informacji',
- 'Database size:' => 'Rozmiar bazy danych :',
- 'Download the database' => 'Pobierz bazę danych',
- 'Optimize the database' => 'Optymalizuj bazę danych',
- '(VACUUM command)' => '(komenda VACUUM)',
- '(Gzip compressed Sqlite file)' => '(baza danych spakowana Gzip)',
- 'User settings' => 'Ustawienia użytkownika',
- 'My default project:' => 'Mój domyślny projekt:',
- 'Close a task' => 'Zakończ zadanie',
- 'Do you really want to close this task: "%s"?' => 'Na pewno chcesz zakończyć to zadanie: "%s"?',
- 'Edit a task' => 'Edytuj zadanie',
- 'Column' => 'Kolumna',
- 'Color' => 'Kolor',
- 'Assignee' => 'Odpowiedzialny',
- 'Create another task' => 'Dodaj kolejne zadanie',
- 'New task' => 'Nowe zadanie',
- '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.',
- 'Add a new task' => 'Dodaj zadanie',
- 'The username is required' => 'Nazwa użytkownika jest wymagana',
- 'The maximum length is %d characters' => 'Maksymalna długość wynosi %d znaków',
- 'The minimum length is %d characters' => 'Minimalna długość wynosi %d znaków',
- 'The password is required' => 'Hasło jest wymagane',
- 'This value must be an integer' => 'Wartość musi być liczbą całkowitą',
- 'The username must be unique' => 'Nazwa użytkownika musi być unikalna',
- 'The username must be alphanumeric' => 'Nazwa użytkownika musi być alfanumeryczna',
- 'The user id is required' => 'ID użytkownika jest wymagane',
- 'Passwords don\'t match' => 'Hasła nie pasują do siebie',
- 'The confirmation is required' => 'Wymagane jest potwierdzenie',
- 'The column is required' => 'Kolumna jest wymagana',
- 'The project is required' => 'Projekt jest wymagany',
- 'The color is required' => 'Kolor jest wymagany',
- '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',
- 'The language is required' => 'Język jest wymagany',
- 'There is no active project, the first step is to create a new project.' => 'Brak aktywnych projektów. Pierwszym krokiem jest utworzenie nowego projektu.',
- 'Settings saved successfully.' => 'Ustawienia zapisane.',
- 'Unable to save your settings.' => 'Nie udało się zapisać ustawień.',
- 'Database optimization done.' => 'Optymalizacja bazy danych zakończona.',
- 'Your project have been created successfully.' => 'Projekt został pomyślnie utworzony.',
- 'Unable to create your project.' => 'Nie udało się stworzyć projektu.',
- 'Project updated successfully.' => 'Projekt zaktualizowany.',
- 'Unable to update this project.' => 'Nie można zaktualizować projektu.',
- 'Unable to remove this project.' => 'Nie można usunąć projektu.',
- 'Project removed successfully.' => 'Projekt usunięty.',
- 'Project activated successfully.' => 'Projekt aktywowany.',
- 'Unable to activate this project.' => 'Nie można aktywować projektu.',
- 'Project disabled successfully.' => 'Projekt wyłączony.',
- 'Unable to disable this project.' => 'Nie można wyłączyć projektu.',
- 'Unable to open this task.' => 'Nie można otworzyć tego zadania.',
- 'Task opened successfully.' => 'Zadanie otwarte.',
- 'Unable to close this task.' => 'Nie można zamknąć tego zadania.',
- 'Task closed successfully.' => 'Zadanie zamknięte.',
- 'Unable to update your task.' => 'Nie można zaktualizować tego zadania.',
- 'Task updated successfully.' => 'Zadanie zaktualizowane.',
- 'Unable to create your task.' => 'Nie można dodać zadania.',
- '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.',
- '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.',
- 'Board updated successfully.' => 'Tablica została zaktualizowana.',
- 'Ready' => 'Gotowe',
- 'Backlog' => 'Log',
- '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' => 'Ident',
- 'No task' => 'Brak zadań',
- 'Completed tasks' => 'Ukończone zadania',
- 'List of projects' => 'Lista projektów',
- 'Completed tasks for "%s"' => 'Zadania zakończone dla "%s"',
- '%d closed tasks' => '%d zamkniętych zadań',
- 'No task for this project' => 'Brak zadań dla tego projektu',
- 'Public link' => 'Link publiczny',
- 'There is no column in your project!' => 'Brak kolumny w Twoim projekcie',
- 'Change assignee' => 'Zmień odpowiedzialną osobę',
- 'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"',
- 'Timezone' => 'Strefa czasowa',
- 'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych',
- 'Page not found' => 'Strona nie istnieje',
- 'Complexity' => 'Poziom trudności',
- 'limit' => 'limit',
- 'Task limit' => 'Limit zadań',
- 'This value must be greater than %d' => 'Wartość musi być większa niż %d',
- 'Edit project access list' => 'Edycja list dostępu dla projektu',
- 'Edit users access' => 'Edytuj dostęp',
- 'Allow this user' => 'Dodaj użytkownika',
- 'Only those users have access to this project:' => 'Użytkownicy mający dostęp:',
- '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.' => '',
- 'You are not allowed to access to this project.' => 'Nie masz dostępu do tego projektu.',
- 'Comments' => 'Komentarze',
- 'Post comment' => 'Dodaj komentarz',
- 'Write your text in Markdown' => 'Możesz użyć Markdown',
- 'Leave a comment' => 'Zostaw komentarz',
- 'Comment is required' => 'Komentarz jest wymagany',
- // 'Leave a description' => '',
- 'Comment added successfully.' => 'Komentarz dodany',
- 'Unable to create your comment.' => 'Nie udało się dodać komentarza',
- 'The description is required' => 'Opis jest wymagany',
- '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',
- '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',
- 'Remove an action' => 'Usuń akcję',
- 'Unable to remove this action.' => 'Nie można usunąć akcji',
- 'Action removed successfully.' => 'Akcja usunięta',
- 'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"',
- 'Defined actions' => 'Zdefiniowane akcje',
- 'Add an action' => 'Nowa akcja',
- 'Event name' => 'Nazwa zdarzenia',
- 'Action name' => 'Nazwa akcji',
- 'Action parameters' => 'Parametry akcji',
- 'Action' => 'Akcja',
- 'Event' => 'Zdarzenie',
- 'When the selected event occurs execute the corresponding action.' => 'Gdy następuje wybrane zdarzenie, uruchom odpowiednią akcję',
- 'Next step' => 'Następny krok',
- 'Define action parameters' => 'Zdefiniuj parametry akcji',
- 'Save this action' => 'Zapisz akcję',
- 'Do you really want to remove this action: "%s"?' => 'Na pewno chcesz usunąć akcję "%s"?',
- 'Remove an automatic action' => 'Usuń akcję automatyczną',
- 'Close the task' => 'Zamknij zadanie',
- 'Assign the task to a specific user' => 'Przypisz zadanie do wybranego użytkownika',
- 'Assign the task to the person who does the action' => 'Przypisz zadanie to osoby wykonującej akcję',
- 'Duplicate the task to another project' => 'Kopiuj zadanie do innego projektu',
- 'Move a task to another column' => 'Przeniesienie zadania do innej kolumny',
- 'Move a task to another position in the same column' => 'Zmiania pozycji zadania w kolumnie',
- 'Task modification' => 'Modyfikacja zadania',
- 'Task creation' => 'Tworzenie zadania',
- 'Open a closed task' => 'Otwarcie zamkniętego zadania',
- 'Closing a task' => 'Zamknięcie zadania',
- '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',
- 'Update this comment' => 'Zapisz komentarz',
- 'Comment updated successfully.' => 'Komentarz został zapisany.',
- 'Unable to update your comment.' => 'Nie udało się zapisanie 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.',
- 'Details' => 'Szczegóły',
- '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',
- 'Reset all tokens' => 'Zresetuj wszystkie tokeny',
- 'All tokens have been regenerated.' => 'Wszystkie tokeny zostały zresetowane.',
- 'Unknown' => 'Nieznany',
- 'Last logins' => 'Ostatnie logowania',
- 'Login date' => 'Data logowania',
- 'Authentication method' => 'Sposób autentykacji',
- 'IP address' => 'Adres IP',
- 'User agent' => 'Przeglądarka',
- 'Persistent connections' => 'Stałe połączenia',
- 'No session.' => 'Brak sesji.',
- 'Expiration date' => 'Data zakończenia',
- 'Remember Me' => 'Pamiętaj mnie',
- 'Creation date' => 'Data utworzenia',
- // 'Filter by user' => '',
- // 'Filter by due date' => '',
- // 'Everybody' => '',
- // 'Open' => '',
- // 'Closed' => '',
- // 'Search' => '',
- // 'Nothing found.' => '',
- // 'Search in the project "%s"' => '',
- // 'Due date' => '',
- // 'Others formats accepted: %s and %s' => '',
- 'Description' => 'Opis',
- // '%d comments' => '',
- // '%d comment' => '',
- // 'Email address invalid' => '',
- // 'Your Google Account is not linked anymore to your profile.' => '',
- // 'Unable to unlink your Google Account.' => '',
- // 'Google authentication failed' => '',
- // 'Unable to link your Google Account.' => '',
- // 'Your Google 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 #%d' => '',
- // 'Task removed successfully.' => '',
- // 'Unable to remove this task.' => '',
- // 'Remove a task' => '',
- // 'Do you really want to remove this task: "%s"?' => '',
- // 'Assign automatically a color based on a category' => '',
- // 'Assign automatically a category based on a color' => '',
- // 'Task creation or modification' => '',
- // 'Category' => '',
- // 'Category:' => '',
- // 'Categories' => '',
- // 'Category not found.' => '',
- // 'Your category have been created successfully.' => '',
- // 'Unable to create your category.' => '',
- // 'Your category have been updated successfully.' => '',
- // 'Unable to update your category.' => '',
- // 'Remove a category' => '',
- // 'Category removed successfully.' => '',
- // 'Unable to remove this category.' => '',
- // 'Category modification for the project "%s"' => '',
- // 'Category Name' => '',
- // 'Categories for the project "%s"' => '',
- // 'Add a new category' => '',
- // 'Do you really want to remove this category: "%s"?' => '',
- // 'Filter by category' => '',
- // '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"?' => '',
- // 'open' => '',
- // 'Attachments' => '',
- // 'Edit the task' => '',
- // 'Edit the description' => '',
- // 'Add a comment' => '',
- // 'Edit a comment' => '',
- // 'Summary' => '',
- // 'Time tracking' => '',
- // 'Estimate:' => '',
- // 'Spent:' => '',
- // 'Do you really want to remove this sub-task?' => '',
- // 'Remaining:' => '',
- // 'hours' => '',
- // 'spent' => '',
- // 'estimated' => '',
- // 'Sub-Tasks' => '',
- // 'Add a sub-task' => '',
- // 'Original estimate' => '',
- // 'Create another sub-task' => '',
- // 'Time spent' => '',
- // 'Edit a sub-task' => '',
- // 'Remove a sub-task' => '',
- // 'The time must be a numeric value' => '',
- // 'Todo' => '',
- // 'In progress' => '',
- // 'Sub-task removed successfully.' => '',
- // 'Unable to remove this sub-task.' => '',
- // 'Sub-task updated successfully.' => '',
- // 'Unable to update your sub-task.' => '',
- // 'Unable to create your sub-task.' => '',
- // 'Sub-task added successfully.' => '',
- // 'Maximum size: ' => '',
- // 'Unable to upload the file.' => '',
- // 'Display another project' => '',
- // 'Your GitHub account was successfully linked to your profile.' => '',
- // 'Unable to link your GitHub Account.' => '',
- // 'GitHub authentication failed' => '',
- // 'Your GitHub account is no longer linked to your profile.' => '',
- // 'Unable to unlink your GitHub Account.' => '',
- // 'Login with my GitHub Account' => '',
- // 'Link my GitHub Account' => '',
- // 'Unlink my GitHub Account' => '',
- // 'Created by %s' => '',
- // 'Last modified on %B %e, %Y at %k:%M %p' => '',
- // 'Tasks Export' => '',
- // 'Tasks exportation for "%s"' => '',
- // 'Start Date' => '',
- // 'End Date' => '',
- // 'Execute' => '',
- // 'Task Id' => '',
- // 'Creator' => '',
- // 'Modification date' => '',
- // 'Completion date' => '',
- // 'Webhook URL for task creation' => '',
- // 'Webhook URL for task modification' => '',
- // 'Clone' => '',
- // 'Clone Project' => '',
- // 'Project cloned successfully.' => '',
- // 'Unable to clone this project.' => '',
- // 'Email notifications' => '',
- // 'Enable email notifications' => '',
- // 'Task position:' => '',
- // 'The task #%d have been opened.' => '',
- // 'The task #%d have been closed.' => '',
- // 'Sub-task updated' => '',
- // 'Title:' => '',
- // 'Status:' => '',
- // 'Assignee:' => '',
- // 'Time tracking:' => '',
- // 'New sub-task' => '',
- // 'New attachment added "%s"' => '',
- // 'Comment updated' => '',
- // 'New comment posted by %s' => '',
- // 'List of due tasks for the project "%s"' => '',
- // '[%s][New attachment] %s (#%d)' => '',
- // '[%s][New comment] %s (#%d)' => '',
- // '[%s][Comment updated] %s (#%d)' => '',
- // '[%s][New subtask] %s (#%d)' => '',
- // '[%s][Subtask updated] %s (#%d)' => '',
- // '[%s][New task] %s (#%d)' => '',
- // '[%s][Task updated] %s (#%d)' => '',
- // '[%s][Task closed] %s (#%d)' => '',
- // '[%s][Task opened] %s (#%d)' => '',
- // '[%s][Due tasks]' => '',
- // '[Kanboard] Notification' => '',
- // 'I want to receive notifications only for those projects:' => '',
- // 'view the task on Kanboard' => '',
- // 'Public access' => '',
- // 'Category management' => '',
- // 'User management' => '',
- // 'Active tasks' => '',
- // 'Disable public access' => '',
- // 'Enable public access' => '',
- // 'Active projects' => '',
- // 'Inactive projects' => '',
- // 'Public access disabled' => '',
- // 'Do you really want to disable this project: "%s"?' => '',
- // 'Do you really want to duplicate this project: "%s"?' => '',
- // 'Do you really want to enable this project: "%s"?' => '',
- // 'Project activation' => '',
- // 'Move the task to another project' => '',
- // 'Move to another project' => '',
- // 'Do you really want to duplicate this task?' => '',
- // 'Duplicate a task' => '',
- // 'External accounts' => '',
- // 'Account type' => '',
- // 'Local' => '',
- // 'Remote' => '',
- // 'Enabled' => '',
- // 'Disabled' => '',
- // 'Google account linked' => '',
- // 'Github account linked' => '',
- // 'Username:' => '',
- // 'Name:' => '',
- // 'Email:' => '',
- // 'Default project:' => '',
- // 'Notifications:' => '',
- // 'Notifications' => '',
- // 'Group:' => '',
- // 'Regular user' => '',
- // '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"' => '',
- // 'Change category' => '',
- // '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
- // '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
- // '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // 'Assigned to %s with an estimate of %s/%sh' => '',
- // 'Not assigned, estimate of %sh' => '',
- // '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s\'s activity' => '',
- // 'No activity.' => '',
- // 'RSS feed' => '',
- // '%s updated a comment on the task #%d' => '',
- // '%s commented on the task #%d' => '',
- // '%s updated a subtask for the task #%d' => '',
- // '%s created a subtask for the task #%d' => '',
- // '%s updated the task #%d' => '',
- // '%s created the task #%d' => '',
- // '%s closed the task #%d' => '',
- // '%s open the task #%d' => '',
- // '%s moved the task #%d to the column "%s"' => '',
- // '%s moved the task #%d to the position %d in the column "%s"' => '',
- // 'Activity' => '',
- // 'Default values are "%s"' => '',
- // 'Default columns for new projects (Comma-separated)' => '',
- // 'Task assignee change' => '',
- // '%s change the assignee of the task #%d to %s' => '',
- // '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '',
- // '[%s][Column Change] %s (#%d)' => '',
- // '[%s][Position Change] %s (#%d)' => '',
- // '[%s][Assignee Change] %s (#%d)' => '',
- // 'New password for the user "%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' => '',
- // 'Label' => '',
- // 'Database' => '',
- // 'About' => '',
- // 'Database driver:' => '',
- // 'Board settings' => '',
- // 'URL and token' => '',
- // 'Webhook settings' => '',
- // 'URL for task creation:' => '',
- // 'Reset token' => '',
- // 'API endpoint:' => '',
- // 'Refresh interval for private board' => '',
- // 'Refresh interval for public board' => '',
- // 'Task highlight period' => '',
- // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
- // 'Frequency in second (60 seconds by default)' => '',
- // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
- // 'Application URL' => '',
- // 'Example: http://example.kanboard.net/ (used by email notifications)' => '',
- // 'Token regenerated.' => '',
- // 'Date format' => '',
- // 'ISO format is always accepted, example: "%s" and "%s"' => '',
- // 'New private project' => '',
- // 'This project is private' => '',
- // 'Type here to create a new sub-task' => '',
- // 'Add' => '',
- // 'Estimated time: %s hours' => '',
- // 'Time spent: %s hours' => '',
- // 'Started on %B %e, %Y' => '',
- // '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.' => '',
-);
diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php
deleted file mode 100644
index 9bfc9cb3..00000000
--- a/app/Locales/pt_BR/translations.php
+++ /dev/null
@@ -1,554 +0,0 @@
-<?php
-
-return array(
- 'None' => 'Nenhum',
- 'edit' => 'editar',
- 'Edit' => 'Editar',
- 'remove' => 'apagar',
- 'Remove' => 'Apagar',
- 'Update' => 'Atualizar',
- 'Yes' => 'Sim',
- 'No' => 'Não',
- 'cancel' => 'cancelar',
- 'or' => 'ou',
- 'Yellow' => 'Amarelo',
- 'Blue' => 'Azul',
- 'Green' => 'Verde',
- 'Purple' => 'Violeta',
- 'Red' => 'Vermelho',
- 'Orange' => 'Laranja',
- 'Grey' => 'Cinza',
- 'Save' => 'Salvar',
- 'Login' => 'Login',
- 'Official website:' => 'Site web oficial :',
- 'Unassigned' => 'Não Atribuída',
- 'View this task' => 'Ver esta tarefa',
- 'Remove user' => 'Remover usuário',
- 'Do you really want to remove this user: "%s"?' => 'Quer realmente remover este usuário: "%s"?',
- 'New user' => 'Novo usuário',
- 'All users' => 'Todos os usuários',
- 'Username' => 'Nome do usuário',
- 'Password' => 'Senha',
- 'Default project' => 'Projeto default',
- 'Administrator' => 'Administrador',
- 'Sign in' => 'Logar',
- 'Users' => 'Usuários',
- 'No user' => 'Sem usuário',
- 'Forbidden' => 'Proibido',
- 'Access Forbidden' => 'Acesso negado',
- 'Only administrators can access to this page.' => 'Somente administradores têm acesso a esta página.',
- 'Edit user' => 'Editar usuário',
- 'Logout' => 'Logout',
- 'Bad username or password' => 'Usuário ou senha inválidos',
- 'users' => 'usuários',
- 'projects' => 'projetos',
- 'Edit project' => 'Editar projeto',
- 'Name' => 'Nome',
- 'Activated' => 'Ativo',
- 'Projects' => 'Projetos',
- 'No project' => 'Nenhum projeto',
- 'Project' => 'Projeto',
- 'Status' => 'Status',
- 'Tasks' => 'Tarefas',
- 'Board' => 'Quadro',
- 'Actions' => 'Ações',
- 'Inactive' => 'Inativo',
- 'Active' => 'Ativo',
- 'Column %d' => 'Coluna %d',
- 'Add this column' => 'Adicionar esta coluna',
- '%d tasks on the board' => '%d tarefas no quadro',
- '%d tasks in total' => '%d tarefas no total',
- 'Unable to update this board.' => 'Impossível atualizar este quadro.',
- 'Edit board' => 'Modificar quadro',
- 'Disable' => 'Desativar',
- 'Enable' => 'Ativar',
- 'New project' => 'Novo projeto',
- 'Do you really want to remove this project: "%s"?' => 'Quer realmente remover este projeto: "%s" ?',
- 'Remove project' => 'Remover projeto',
- 'Boards' => 'Quadros',
- 'Edit the board for "%s"' => 'Editar o quadro para "%s"',
- 'All projects' => 'Todos os projetos',
- 'Change columns' => 'Modificar colunas',
- 'Add a new column' => 'Adicionar uma nova coluna',
- 'Title' => 'Título',
- 'Add Column' => 'Adicionar coluna',
- 'Project "%s"' => 'Projeto "%s"',
- 'Nobody assigned' => 'Ninguém designado',
- 'Assigned to %s' => 'Designado para %s',
- 'Remove a column' => 'Remover uma coluna',
- 'Remove a column from a board' => 'Remover uma coluna do quadro',
- 'Unable to remove this column.' => 'Impossível remover esta coluna.',
- 'Do you really want to remove this column: "%s"?' => 'Quer realmente remover esta coluna: "%s"?',
- 'This action will REMOVE ALL TASKS associated to this column!' => 'Esta ação vai REMOVER TODAS AS TAREFAS associadas a esta coluna!',
- 'Settings' => 'Preferências',
- 'Application settings' => 'Preferências da aplicação',
- 'Language' => 'Idioma',
- 'Webhook token:' => 'Token de webhooks:',
- 'API token:' => 'API Token:',
- 'More information' => 'Mais informação',
- 'Database size:' => 'Tamanho do banco de dados:',
- 'Download the database' => 'Download do banco de dados',
- 'Optimize the database' => 'Otimizar o banco de dados',
- '(VACUUM command)' => '(Comando VACUUM)',
- '(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)',
- 'User settings' => 'Configurações do usuário',
- 'My default project:' => 'Meu projeto default:',
- 'Close a task' => 'Encerrar uma tarefa',
- 'Do you really want to close this task: "%s"?' => 'Quer realmente encerrar esta tarefa: "%s"?',
- 'Edit a task' => 'Editar uma tarefa',
- 'Column' => 'Coluna',
- 'Color' => 'Cor',
- 'Assignee' => 'Designação',
- 'Create another task' => 'Criar uma outra tarefa (aproveitando os dados preenchidos)',
- 'New task' => 'Nova tarefa',
- 'Open a task' => 'Abrir uma tarefa',
- 'Do you really want to open this task: "%s"?' => 'Quer realmente 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 designado',
- 'Column on the board:' => 'Coluna no quadro:',
- 'Status is open' => 'Status está aberto',
- 'Status is closed' => 'Status está fechado',
- 'Close this task' => 'Fechar esta tarefa',
- 'Open this task' => 'Abrir esta tarefa',
- 'There is no description.' => 'Não há descrição.',
- 'Add a new task' => 'Adicionar uma nova tarefa',
- 'The username is required' => 'O nome de usuário é obrigatório',
- 'The maximum length is %d characters' => 'O tamanho máximo são %d caracteres',
- 'The minimum length is %d characters' => 'O tamanho mínimo são %d caracteres',
- 'The password is required' => 'A senha é obrigatória',
- 'This value must be an integer' => 'O valor deve ser um inteiro',
- 'The username must be unique' => 'O nome de usuário deve ser único',
- 'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico, sem espaços ou _',
- 'The user id is required' => 'O id de usuário é obrigatório',
- 'Passwords don\'t match' => 'As senhas não conferem',
- 'The confirmation is required' => 'A confirmação é obrigatória',
- 'The column is required' => 'A coluna é obrigatória',
- 'The project is required' => 'O projeto é obrigatório',
- 'The color is required' => 'A cor é obrigatória',
- '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',
- 'The language is required' => 'O idioma é obrigatório',
- 'There is no active project, the first step is to create a new project.' => 'Não há projeto ativo. O primeiro passo é criar um novo projeto.',
- 'Settings saved successfully.' => 'Configurações salvas com sucesso.',
- 'Unable to save your settings.' => 'Impossível salvar suas configurações.',
- 'Database optimization done.' => 'Otimização do banco de dados terminada.',
- 'Your project have been created successfully.' => 'Seu projeto foi criado com sucesso.',
- 'Unable to create your project.' => 'Impossível criar seu projeto.',
- 'Project updated successfully.' => 'Projeto atualizado com sucesso.',
- 'Unable to update this project.' => 'Impossível atualizar este projeto.',
- 'Unable to remove this project.' => 'Impossível remover este projeto.',
- 'Project removed successfully.' => 'Projeto removido com sucesso.',
- 'Project activated successfully.' => 'Projeto ativado com sucesso.',
- 'Unable to activate this project.' => 'Impossível ativar este projeto.',
- 'Project disabled successfully.' => 'Projeto desabilitado com sucesso.',
- 'Unable to disable this project.' => 'Impossível desabilitar este projeto.',
- 'Unable to open this task.' => 'Impossível abrir esta tarefa.',
- 'Task opened successfully.' => 'Tarefa aberta com sucesso.',
- 'Unable to close this task.' => 'Impossível encerrar esta tarefa.',
- 'Task closed successfully.' => 'Tarefa encerrada com sucesso.',
- 'Unable to update your task.' => 'Impossível atualizar sua tarefa.',
- 'Task updated successfully.' => 'Tarefa atualizada com sucesso.',
- 'Unable to create your task.' => 'Impossível criar sua tarefa.',
- 'Task created successfully.' => 'Tarefa criada com sucesso.',
- 'User created successfully.' => 'Usuário criado com sucesso.',
- 'Unable to create your user.' => 'Impossível criar seu usuário.',
- 'User updated successfully.' => 'Usuário atualizado com sucesso.',
- 'Unable to update your user.' => 'Impossível atualizar seu usuário.',
- 'User removed successfully.' => 'Usuário removido com sucesso.',
- 'Unable to remove this user.' => 'Impossível remover este usuário.',
- 'Board updated successfully.' => 'Quadro atualizado com sucesso.',
- 'Ready' => 'Pronto',
- 'Backlog' => 'Backlog',
- 'Work in progress' => 'Em andamento',
- 'Done' => 'Encerrado',
- 'Application version:' => 'Versão da aplicação:',
- 'Completed on %B %e, %Y at %k:%M %p' => 'Encerrado 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 de encerramento',
- 'Id' => 'Id',
- 'No task' => 'Nenhuma tarefa',
- 'Completed tasks' => 'tarefas completadas',
- 'List of projects' => 'Lista de projetos',
- 'Completed tasks for "%s"' => 'Tarefas completadas por "%s"',
- '%d closed tasks' => '%d tarefas encerradas',
- 'No task for this project' => 'Nenhuma tarefa para este projeto',
- 'Public link' => 'Link público',
- 'There is no column in your project!' => 'Não há colunas no seu projeto!',
- 'Change assignee' => 'Mudar a designação',
- 'Change assignee for the task "%s"' => 'Modificar designação para a tarefa "%s"',
- 'Timezone' => 'Fuso horário',
- 'Sorry, I didn\'t found 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',
- 'limit' => 'limite',
- 'Task limit' => 'Limite da tarefa',
- 'This value must be greater than %d' => 'Este valor deve ser maior que %d',
- 'Edit project access list' => 'Editar lista de acesso ao projeto',
- 'Edit users access' => 'Editar acesso de usuários',
- 'Allow this user' => 'Permitir esse usuário',
- 'Only those users have access to this project:' => 'Somente estes usuários têm acesso a este projeto:',
- '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.' => '',
- 'You are not allowed to access to this project.' => 'Você não está autorizado a acessar este projeto.',
- 'Comments' => 'Comentários',
- 'Post comment' => 'Postar comentário',
- 'Write your text in Markdown' => 'Escreva seu texto em Markdown',
- 'Leave a comment' => 'Deixe um comentário',
- 'Comment is required' => 'Comentário é obrigatório',
- 'Leave a description' => 'Deixe uma descrição',
- 'Comment added successfully.' => 'Cpmentário adicionado com sucesso.',
- 'Unable to create your comment.' => 'Impossível criar seu comentário.',
- 'The description is required' => 'A descrição é obrigatória',
- '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 feito antes de %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.' => 'Impossível criar sua ação automática.',
- 'Remove an action' => 'Remover uma ação',
- 'Unable to remove this action.' => 'Impossível remover esta ação',
- 'Action removed successfully.' => 'Ação removida com sucesso.',
- 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"',
- 'Defined actions' => 'Ações definidas',
- 'Add an action' => 'Adicionar Ação',
- 'Event name' => 'Nome do evento',
- 'Action name' => 'Nome da ação',
- 'Action parameters' => 'Parâmetros da ação',
- 'Action' => 'Ação',
- 'Event' => 'Evento',
- 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer, execute a ação correspondente',
- 'Next step' => 'Próximo passo',
- 'Define action parameters' => 'Definir parêmetros da ação',
- 'Save this action' => 'Salvar esta ação',
- 'Do you really want to remove this action: "%s"?' => 'Você quer realmente remover esta ação: "%s"?',
- 'Remove an automatic action' => 'Remove uma ação automática',
- 'Close the task' => 'Fechar tarefa',
- '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',
- 'Move a task to another column' => 'Mover a tarefa para outra coluna',
- 'Move a task to another position in the same column' => 'Mover a tarefa para outra posição, na mesma coluna',
- 'Task modification' => 'Modificação de tarefa',
- 'Task creation' => 'Criação de tarefa',
- 'Open a closed task' => 'Reabrir uma tarefa fechada',
- 'Closing a task' => 'Fechando uma tarefa',
- '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',
- 'Update this comment' => 'Atualizar este comentário',
- 'Comment updated successfully.' => 'Comentário atualizado com sucesso.',
- 'Unable to update your comment.' => 'Impossível atualizar seu comentário.',
- 'Remove a comment' => 'Remover um comentário.',
- 'Comment removed successfully.' => 'Comentário removido com sucesso.',
- 'Unable to remove this comment.' => 'Impossível remover este comentário',
- 'Do you really want to remove this comment?' => 'Você tem certeza de que quer remover este comentário?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Somente administradores ou o criator deste comentário tem acesso a esta página.',
- 'Details' => 'Detalhes',
- '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 errada',
- 'Reset all tokens' => 'Reiniciar todos os tokens',
- 'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente',
- 'Unknown' => 'Desconhecido',
- 'Last logins' => 'Últimos logins',
- 'Login date' => 'Data de login',
- 'Authentication method' => 'Método de autenticação',
- 'IP address' => 'Endereço IP',
- 'User agent' => 'Agente usuário',
- 'Persistent connections' => 'Conexões persistentes',
- 'No session.' => 'Sem sessão.',
- 'Expiration date' => 'Data de expiração',
- 'Remember Me' => 'Lembre-se de mim',
- 'Creation date' => 'Data de criação',
- 'Filter by user' => 'Filtrar por usuário',
- 'Filter by due date' => 'Filtrar por data de vencimento',
- 'Everybody' => 'Todos',
- 'Open' => 'Abrir',
- 'Closed' => 'Fechado',
- 'Search' => 'Pesquisar',
- 'Nothing found.' => 'Não encontrado.',
- 'Search in the project "%s"' => 'Procure no projeto "%s"',
- 'Due date' => 'Data de vencimento',
- 'Others formats accepted: %s and %s' => 'Outros formatos permitidos: %s e %s',
- 'Description' => 'Descrição',
- '%d comments' => '%d comentários',
- '%d comment' => '%d comentário',
- 'Email address invalid' => 'Endereço de e-mail inválido',
- 'Your Google Account is not linked anymore to your profile.' => 'Sua conta Google não está mais associada ao seu perfil.',
- 'Unable to unlink your Google Account.' => 'Impossível desassociar sua conta Google.',
- 'Google authentication failed' => 'Autenticação do Google falhou.',
- 'Unable to link your Google Account.' => 'Impossível associar a sua conta do Google.',
- 'Your Google Account is linked to your profile successfully.' => 'Sua Conta do Google está ligada ao seu perfil com sucesso.',
- 'Email' => 'E-mail',
- 'Link my Google Account' => 'Vincular minha conta 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 #%d' => 'Tarefa #%d',
- '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"',
- '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',
- 'Category' => 'Categoria',
- 'Category:' => 'Categoria:',
- 'Categories' => 'Categorias',
- 'Category not found.' => 'Categoria não encontrada.',
- 'Your category have been created successfully.' => 'Seu categoria foi criada com sucesso.',
- 'Unable to create your category.' => 'Não é possível criar sua categoria.',
- '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.',
- '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',
- 'Categories for the project "%s"' => 'Categorias para o projeto "%s"',
- 'Add a new category' => 'Adicionar uma nova categoria',
- 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"',
- 'Filter by category' => 'Filtrar por categoria',
- 'All categories' => 'Todas as categorias',
- 'No category' => 'Sem categoria',
- 'The name is required' => 'O nome é obrigatório',
- 'Remove a file' => 'Remover um arquivo',
- '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"',
- 'open' => 'Aberto',
- 'Attachments' => 'Anexos',
- 'Edit the task' => 'Editar a tarefa',
- 'Edit the description' => 'Editar a descrição',
- 'Add a comment' => 'Adicionar um comentário',
- 'Edit a comment' => 'Editar um comentário',
- 'Summary' => 'Resumo',
- 'Time tracking' => 'Rastreamento de tempo',
- 'Estimate:' => 'Estimado:',
- 'Spent:' => 'Gasto:',
- 'Do you really want to remove this sub-task?' => 'Você realmente deseja remover esta sub-tarefa?',
- 'Remaining:' => 'Restante:',
- 'hours' => 'horas',
- 'spent' => 'gasto',
- 'estimated' => 'estimada',
- 'Sub-Tasks' => 'Sub-tarefas',
- 'Add a sub-task' => 'Adicionar uma sub-tarefa',
- 'Original estimate' => 'Estimativa original',
- 'Create another sub-task' => 'Criar uma outra sub-tarefa',
- 'Time spent' => 'Tempo gasto',
- 'Edit a sub-task' => 'Editar uma sub-tarefa',
- 'Remove a sub-task' => 'Remover uma sub-tarefa',
- 'The time must be a numeric value' => 'O tempo deve ser um valor numérico',
- 'Todo' => 'A fazer',
- 'In progress' => 'Em andamento',
- 'Sub-task removed successfully.' => 'Sub-tarefa removido com sucesso.',
- 'Unable to remove this sub-task.' => 'Não foi possível remover esta sub-tarefa.',
- 'Sub-task updated successfully.' => 'Sub-tarefa atualizada com sucesso.',
- 'Unable to update your sub-task.' => 'Não foi possível atualizar sua sub-tarefa.',
- 'Unable to create your sub-task.' => 'Não é possível criar sua sub-tarefa.',
- 'Sub-task added successfully.' => 'Sub-tarefa adicionada com sucesso.',
- 'Maximum size: ' => 'O tamanho máximo:',
- 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.',
- 'Display another project' => 'Mostrar um outro projeto',
- 'Your GitHub account was successfully linked to your profile.' => 'A sua conta GitHub foi ligada com sucesso ao seu perfil.',
- 'Unable to link your GitHub Account.' => 'Não foi possível vincular sua conta GitHub.',
- 'GitHub authentication failed' => 'Falhou autenticação GitHub',
- 'Your GitHub account is no longer linked to your profile.' => 'A sua conta GitHub já não está ligada ao seu perfil.',
- 'Unable to unlink your GitHub Account.' => 'Não foi possível desvincular sua conta GitHub.',
- 'Login with my GitHub Account' => 'Entrar com minha conta do GitHub',
- 'Link my GitHub Account' => 'Vincular minha conta GitHub',
- 'Unlink my GitHub Account' => 'Desvincular 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' => 'Tarefas Export',
- 'Tasks exportation for "%s"' => 'Tarefas exportação para "%s"',
- 'Start Date' => 'Data inicial',
- 'End Date' => 'Data final',
- 'Execute' => 'Executar',
- 'Task Id' => 'Id da Tarefa',
- 'Creator' => 'Criador',
- 'Modification date' => 'Data de modificação',
- 'Completion date' => 'Data de conclusão',
- 'Webhook URL for task creation' => 'Webhook URL para criação de tarefas',
- 'Webhook URL for task modification' => 'Webhook URL para modificação tarefa',
- 'Clone' => 'Clone',
- 'Clone Project' => 'Clonar Projeto',
- 'Project cloned successfully.' => 'Projeto clonado com sucesso.',
- 'Unable to clone this project.' => 'Impossível clonar este projeto.',
- // 'Email notifications' => '',
- // 'Enable email notifications' => '',
- // 'Task position:' => '',
- // 'The task #%d have been opened.' => '',
- // 'The task #%d have been closed.' => '',
- // 'Sub-task updated' => '',
- // 'Title:' => '',
- // 'Status:' => '',
- // 'Assignee:' => '',
- // 'Time tracking:' => '',
- // 'New sub-task' => '',
- // 'New attachment added "%s"' => '',
- // 'Comment updated' => '',
- // 'New comment posted by %s' => '',
- // 'List of due tasks for the project "%s"' => '',
- // '[%s][New attachment] %s (#%d)' => '',
- // '[%s][New comment] %s (#%d)' => '',
- // '[%s][Comment updated] %s (#%d)' => '',
- // '[%s][New subtask] %s (#%d)' => '',
- // '[%s][Subtask updated] %s (#%d)' => '',
- // '[%s][New task] %s (#%d)' => '',
- // '[%s][Task updated] %s (#%d)' => '',
- // '[%s][Task closed] %s (#%d)' => '',
- // '[%s][Task opened] %s (#%d)' => '',
- // '[%s][Due tasks]' => '',
- // '[Kanboard] Notification' => '',
- // 'I want to receive notifications only for those projects:' => '',
- // 'view the task on Kanboard' => '',
- // 'Public access' => '',
- // 'Category management' => '',
- // 'User management' => '',
- // 'Active tasks' => '',
- // 'Disable public access' => '',
- // 'Enable public access' => '',
- // 'Active projects' => '',
- // 'Inactive projects' => '',
- // 'Public access disabled' => '',
- // 'Do you really want to disable this project: "%s"?' => '',
- // 'Do you really want to duplicate this project: "%s"?' => '',
- // 'Do you really want to enable this project: "%s"?' => '',
- // 'Project activation' => '',
- // 'Move the task to another project' => '',
- // 'Move to another project' => '',
- // 'Do you really want to duplicate this task?' => '',
- // 'Duplicate a task' => '',
- // 'External accounts' => '',
- // 'Account type' => '',
- // 'Local' => '',
- // 'Remote' => '',
- // 'Enabled' => '',
- // 'Disabled' => '',
- // 'Google account linked' => '',
- // 'Github account linked' => '',
- // 'Username:' => '',
- // 'Name:' => '',
- // 'Email:' => '',
- // 'Default project:' => '',
- // 'Notifications:' => '',
- // 'Notifications' => '',
- // 'Group:' => '',
- // 'Regular user' => '',
- // '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"' => '',
- // 'Change category' => '',
- // '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '',
- // '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '',
- // '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // 'Assigned to %s with an estimate of %s/%sh' => '',
- // 'Not assigned, estimate of %sh' => '',
- // '%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '',
- // '%s\'s activity' => '',
- // 'No activity.' => '',
- // 'RSS feed' => '',
- // '%s updated a comment on the task #%d' => '',
- // '%s commented on the task #%d' => '',
- // '%s updated a subtask for the task #%d' => '',
- // '%s created a subtask for the task #%d' => '',
- // '%s updated the task #%d' => '',
- // '%s created the task #%d' => '',
- // '%s closed the task #%d' => '',
- // '%s open the task #%d' => '',
- // '%s moved the task #%d to the column "%s"' => '',
- // '%s moved the task #%d to the position %d in the column "%s"' => '',
- // 'Activity' => '',
- // 'Default values are "%s"' => '',
- // 'Default columns for new projects (Comma-separated)' => '',
- // 'Task assignee change' => '',
- // '%s change the assignee of the task #%d to %s' => '',
- // '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '',
- // '[%s][Column Change] %s (#%d)' => '',
- // '[%s][Position Change] %s (#%d)' => '',
- // '[%s][Assignee Change] %s (#%d)' => '',
- // 'New password for the user "%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' => '',
- // 'Label' => '',
- // 'Database' => '',
- // 'About' => '',
- // 'Database driver:' => '',
- // 'Board settings' => '',
- // 'URL and token' => '',
- // 'Webhook settings' => '',
- // 'URL for task creation:' => '',
- // 'Reset token' => '',
- // 'API endpoint:' => '',
- // 'Refresh interval for private board' => '',
- // 'Refresh interval for public board' => '',
- // 'Task highlight period' => '',
- // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
- // 'Frequency in second (60 seconds by default)' => '',
- // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
- // 'Application URL' => '',
- // 'Example: http://example.kanboard.net/ (used by email notifications)' => '',
- // 'Token regenerated.' => '',
- // 'Date format' => '',
- // 'ISO format is always accepted, example: "%s" and "%s"' => '',
- // 'New private project' => '',
- // 'This project is private' => '',
- // 'Type here to create a new sub-task' => '',
- // 'Add' => '',
- // 'Estimated time: %s hours' => '',
- // 'Time spent: %s hours' => '',
- // 'Started on %B %e, %Y' => '',
- // '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.' => '',
-);
diff --git a/app/Locales/ru_RU/translations.php b/app/Locales/ru_RU/translations.php
deleted file mode 100644
index 9d236947..00000000
--- a/app/Locales/ru_RU/translations.php
+++ /dev/null
@@ -1,554 +0,0 @@
-<?php
-
-return array(
- 'None' => 'Отсутствует',
- 'edit' => 'изменить',
- 'Edit' => 'Изменить',
- 'remove' => 'удалить',
- 'Remove' => 'Удалить',
- 'Update' => 'Обновить',
- 'Yes' => 'Да',
- 'No' => 'Нет',
- 'cancel' => 'Отменить',
- 'or' => 'или',
- 'Yellow' => 'Желтый',
- 'Blue' => 'Синий',
- 'Green' => 'Зеленый',
- 'Purple' => 'Фиолетовый',
- 'Red' => 'Красный',
- 'Orange' => 'Оранжевый',
- 'Grey' => 'Серый',
- 'Save' => 'Сохранить',
- 'Login' => 'Вход',
- 'Official website:' => 'Официальный сайт :',
- 'Unassigned' => 'Не назначена',
- 'View this task' => 'Посмотреть задачу',
- 'Remove user' => 'Удалить пользователя',
- 'Do you really want to remove this user: "%s"?' => 'Вы точно хотите удалить пользователя: « %s » ?',
- 'New user' => 'Новый пользователь',
- 'All users' => 'Все пользователи',
- 'Username' => 'Имя пользователя',
- 'Password' => 'Пароль',
- 'Default project' => 'Проект по умолчанию',
- 'Administrator' => 'Администратор',
- 'Sign in' => 'Войти',
- 'Users' => 'Пользователи',
- 'No user' => 'Нет пользователя',
- 'Forbidden' => 'Запрещено',
- 'Access Forbidden' => 'Доступ запрещен',
- 'Only administrators can access to this page.' => 'Только администраторы могут войти на эту страницу.',
- 'Edit user' => 'Изменить пользователя',
- 'Logout' => 'Выйти',
- 'Bad username or password' => 'Неверное имя пользователя или пароль',
- 'users' => 'пользователи',
- 'projects' => 'проекты',
- 'Edit project' => 'Изменить проект',
- 'Name' => 'Имя',
- 'Activated' => 'Активен',
- 'Projects' => 'Проекты',
- 'No project' => 'Нет проекта',
- 'Project' => 'Проект',
- 'Status' => 'Статус',
- 'Tasks' => 'Задачи',
- 'Board' => 'Доска',
- 'Actions' => 'Действия',
- 'Inactive' => 'Неактивен',
- 'Active' => 'Активен',
- 'Column %d' => 'Колонка %d',
- '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' => 'Удалить проект',
- 'Boards' => 'Доски',
- 'Edit the board for "%s"' => 'Изменить доску для « %s »',
- 'All projects' => 'Все проекты',
- 'Change columns' => 'Изменить колонки',
- 'Add a new column' => 'Добавить новую колонку',
- 'Title' => 'Название',
- 'Add Column' => 'Добавить колонку',
- 'Project "%s"' => 'Проект « %s »',
- '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:' => 'Webhooks токен :',
- 'API token:' => 'API токен :',
- 'More information' => 'Подробнее',
- 'Database size:' => 'Размер базы данных :',
- 'Download the database' => 'Скачать базу данных',
- 'Optimize the database' => 'Оптимизировать базу данных',
- '(VACUUM command)' => '(Команда VACUUM)',
- '(Gzip compressed Sqlite file)' => '(Сжать GZip файл SQLite)',
- 'User settings' => 'Настройки пользователя',
- 'My default project:' => 'Мой проект по умолчанию : ',
- 'Close a task' => 'Закрыть задачу',
- 'Do you really want to close this task: "%s"?' => 'Вы точно хотите закрыть задачу : « %s » ?',
- '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' => 'Вернуться на доску',
- '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.' => 'Нет описания.',
- '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 username must be alphanumeric' => 'Имя пользователя должно быть букво-цифровым',
- 'The user id is required' => 'Требуется ID пользователя',
- 'Passwords don\'t match' => 'Пароли не совпадают',
- 'The confirmation is required' => 'Требуется подтверждение',
- 'The column is required' => 'Требуется колонка',
- 'The project is required' => 'Требуется проект',
- 'The color 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' => 'Требуется заголовок',
- 'The language is required' => 'Требуется язык',
- 'There is no active project, the first step is to create a new project.' => 'Нет активного проекта, сначала создайте новый проект.',
- '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:' => 'Версия приложения :',
- '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',
- 'No task' => 'Нет задачи',
- 'Completed tasks' => 'Завершенные задачи',
- 'List of projects' => 'Список проектов',
- 'Completed tasks for "%s"' => 'Задачи завершенные для « %s »',
- '%d closed tasks' => '%d завершенных задач',
- 'No task for this project' => 'нет задач для этого проекта',
- 'Public link' => 'Ссылка для просмотра',
- 'There is no column in your project!' => 'Нет колонки в вашем проекте !',
- 'Change assignee' => 'Сменить назначенного',
- 'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »',
- 'Timezone' => 'Часовой пояс',
- 'Sorry, I didn\'t found this information in my database!' => 'К сожалению, информация в базе данных не найдена !',
- 'Page not found' => 'Страница не найдена',
- 'Complexity' => 'Сложность',
- 'limit' => 'лимит',
- 'Task limit' => 'Лимит задач',
- 'This value must be greater than %d' => 'Это значение должно быть больше %d',
- 'Edit project access list' => 'Изменить доступ к проекту',
- 'Edit users access' => 'Изменить доступ пользователей',
- 'Allow this user' => 'Разрешить этого пользователя',
- 'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту :',
- 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет доступ ко всему.',
- 'revoke' => 'отозвать',
- 'List of authorized users' => 'Список авторизованных пользователей',
- 'User' => 'Пользователь',
- 'Nobody have access to this project.' => 'Ни у кого нет доступа к этому проекту',
- 'You are not allowed to access to this project.' => 'Вам запрешен доступ к этому проекту.',
- 'Comments' => 'Комментарии',
- 'Post comment' => 'Оставить комментарий',
- 'Write your text in Markdown' => 'Справка по синтаксису Markdown',
- 'Leave a comment' => 'Оставить комментарий 2',
- 'Comment is required' => 'Нужен комментарий',
- 'Leave a description' => 'Оставьте описание',
- 'Comment added successfully.' => 'Комментарий успешно добавлен.',
- 'Unable to create your comment.' => 'Невозможно создать комментарий.',
- 'The description is required' => 'Требуется описание',
- 'Edit this task' => 'Изменить задачу',
- 'Due Date' => 'Сделать до',
- 'Invalid date' => 'Неверная дата',
- 'Must be done before %B %e, %Y' => 'Должно быть сделано до %d/%m/%Y',
- '%B %e, %Y' => '%d/%m/%Y',
- 'Automatic actions' => 'Автоматические действия',
- 'Your automatic action have been created successfully.' => 'Автоматика настроена.',
- 'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.',
- 'Remove an action' => 'Удалить действие',
- 'Unable to remove this action.' => 'Не удалось удалить действие',
- 'Action removed successfully.' => 'Действие удалено.',
- 'Automatic actions for the project "%s"' => 'Автоматические действия для проекта « %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' => 'Удалить автоматическое действие',
- 'Close the task' => 'Закрыть задачу',
- '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' => 'Переместить задачу в другую колонку',
- 'Move a task to another position in the same column' => 'Переместить задачу в другое место этой же колонки',
- 'Task modification' => 'Изменение задачи',
- 'Task creation' => 'Создание задачи',
- 'Open a closed task' => 'Открыть завершенную задачу',
- 'Closing a task' => 'Завершение задачи',
- 'Assign a color to a specific user' => 'Назначить определенный цвет пользователю',
- 'Column title' => 'Название колонки',
- 'Position' => 'Расположение',
- 'Move Up' => 'Сдвинуть вверх',
- 'Move Down' => 'Сдвинуть вниз',
- 'Duplicate to another project' => 'Клонировать в другой проект',
- 'Duplicate' => 'Клонировать',
- 'link' => 'ссылка',
- 'Update this comment' => 'Обновить комментарий',
- '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?' => 'Вы точно хотите удалить этот комментарий ?',
- 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор или автор комментарий могут получить доступ.',
- 'Details' => 'Подробности',
- 'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »',
- 'The current password is required' => 'Требуется текущий пароль',
- 'Wrong password' => 'Неверный пароль',
- 'Reset all tokens' => 'Сброс всех токенов',
- 'All tokens have been regenerated.' => 'Все токены пересозданы.',
- 'Unknown' => 'Неизвестно',
- 'Last logins' => 'Последние посещения',
- 'Login date' => 'Дата входа',
- 'Authentication method' => 'Способ аутентификации',
- 'IP address' => 'IP адрес',
- 'User agent' => 'User agent',
- 'Persistent connections' => 'Постоянные соединения',
- 'No session.' => 'Нет сеанса',
- 'Expiration date' => 'Дата окончания',
- 'Remember Me' => 'Запомнить меня',
- 'Creation date' => 'Дата создания',
- 'Filter by user' => 'Фильтр по пользователям',
- 'Filter by due date' => 'Фильтр по сроку',
- 'Everybody' => 'Все',
- 'Open' => 'Открытый',
- 'Closed' => 'Закрытый',
- 'Search' => 'Поиск',
- 'Nothing found.' => 'Ничего не найдено.',
- 'Search in the project "%s"' => 'Искать в проекте « %s »',
- 'Due date' => 'Срок',
- 'Others formats accepted: %s and %s' => 'Другой формат приемлем : %s и %s',
- 'Description' => 'Описание',
- '%d comments' => '%d комментариев',
- '%d comment' => '%d комментарий',
- 'Email address invalid' => 'Adresse email invalide',
- 'Your Google Account is not linked anymore to your profile.' => 'Ваш аккаунт в Google больше не привязан к вашему профилю.',
- 'Unable to unlink your Google Account.' => 'Не удалось отвязать ваш профиль от Google.',
- 'Google authentication failed' => 'Аутентификация Google не удалась',
- 'Unable to link your Google Account.' => 'Не удалось привязать ваш профиль к Google.',
- 'Your Google Account is linked to your profile successfully.' => 'Ваш профиль успешно привязан к Google.',
- 'Email' => 'Email',
- 'Link my Google Account' => 'Привязать мой профиль к Google',
- 'Unlink my Google Account' => 'Отвязать мой профиль от Google',
- 'Login with my Google Account' => 'Аутентификация через Google',
- 'Project not found.' => 'Проект не найден.',
- 'Task #%d' => 'Задача n°%d',
- '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' => 'Название категории',
- 'Categories for the project "%s"' => 'Категории для проекта « %s »',
- 'Add a new category' => 'Добавить новую категорию',
- 'Do you really want to remove this category: "%s"?' => 'Вы точно хотите удалить категорию « %s » ?',
- 'Filter by category' => 'Фильтр по категориям',
- '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 » ?',
- 'open' => 'открыть',
- 'Attachments' => 'Приложение',
- 'Edit the task' => 'Изменить задачу',
- 'Edit the description' => 'Изменить описание',
- 'Add a comment' => 'Добавить комментарий',
- 'Edit a comment' => 'Изменить комментарий',
- 'Summary' => 'Сводка',
- 'Time tracking' => 'Отслеживание времени',
- 'Estimate:' => 'Приблизительно :',
- 'Spent:' => 'Затрачено :',
- 'Do you really want to remove this sub-task?' => 'Вы точно хотите удалить подзадачу ?',
- 'Remaining:' => 'Осталось :',
- 'hours' => 'часов',
- 'spent' => 'затрачено',
- 'estimated' => 'расчетное',
- 'Sub-Tasks' => 'Подзадачи',
- 'Add a sub-task' => 'Добавить подзадачу',
- 'Original estimate' => 'Первичная оценка',
- 'Create another sub-task' => 'Создать другую подзадачу',
- 'Time spent' => 'Времени затрачено',
- 'Edit a sub-task' => 'Изменить подзадачу',
- 'Remove a sub-task' => 'Удалить подзадачу',
- 'The time must be a numeric value' => 'Время должно быть числом!',
- 'Todo' => 'К исполнению',
- 'In progress' => 'В процессе',
- 'Sub-task removed successfully.' => 'Подзадача удалена.',
- 'Unable to remove this sub-task.' => 'Не удалось удалить подзадачу.',
- 'Sub-task updated successfully.' => 'Подзадача обновлена.',
- 'Unable to update your sub-task.' => 'Не удалось обновить подзадачу.',
- 'Unable to create your sub-task.' => 'Не удалось создать подзадачу.',
- 'Sub-task added successfully.' => 'Подзадача добавлена.',
- 'Maximum size: ' => 'Максимальный размер : ',
- 'Unable to upload the file.' => 'Не удалось загрузить файл.',
- 'Display another project' => 'Показать другой проект',
- 'Your GitHub account was successfully linked to your profile.' => 'Ваш GitHub привязан к вашему профилю.',
- 'Unable to link your GitHub Account.' => 'Не удалось привязать ваш профиль к Github.',
- 'GitHub authentication failed' => 'Аутентификация в GitHub не удалась',
- 'Your GitHub account is no longer linked to your profile.' => 'Ваш GitHub отвязан от вашего профиля.',
- 'Unable to unlink your GitHub Account.' => 'Не удалось отвязать ваш профиль от GitHub.',
- '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' => 'Дата начала',
- 'End Date' => 'Дата завершения',
- 'Execute' => 'Выполнить',
- 'Task Id' => 'ID задачи',
- 'Creator' => 'Автор',
- 'Modification date' => 'Дата изменения',
- 'Completion date' => 'Дата завершения',
- 'Webhook URL for task creation' => 'Webhook URL для создания задачи',
- 'Webhook URL for task modification' => 'Webhook URL для изменения задачи',
- 'Clone' => 'Клонировать',
- 'Clone Project' => 'Клонировать проект',
- 'Project cloned successfully.' => 'Проект клонирован.',
- 'Unable to clone this project.' => 'Не удалось клонировать проект.',
- 'Email notifications' => 'Уведомления по email',
- 'Enable email notifications' => 'Включить уведомления по email',
- '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 »',
- 'List of due tasks for the project "%s"' => 'Список сроков к проекту « %s »',
- '[%s][New attachment] %s (#%d)' => '[%s][Новых вложений] %s (#%d)',
- '[%s][New comment] %s (#%d)' => '[%s][Новых комментариев] %s (#%d)',
- '[%s][Comment updated] %s (#%d)' => '[%s][Обновленых коментариев] %s (#%d)',
- '[%s][New subtask] %s (#%d)' => '[%s][Новых подзадач] %s (#%d)',
- '[%s][Subtask updated] %s (#%d)' => '[%s][Обновленных подзадач] %s (#%d)',
- '[%s][New task] %s (#%d)' => '[%s][Новых задач] %s (#%d)',
- '[%s][Task updated] %s (#%d)' => '[%s][Обновленных задач] %s (#%d)',
- '[%s][Task closed] %s (#%d)' => '[%s][Закрытых задач] %s (#%d)',
- '[%s][Task opened] %s (#%d)' => '[%s][Открытых задач] %s (#%d)',
- '[%s][Due tasks]' => '[%s][Текущие задачи]',
- '[Kanboard] Notification' => '[Kanboard] Оповещение',
- 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам :',
- 'view the task on Kanboard' => 'посмотреть задачу на Kanboard',
- 'Public access' => 'Общий доступ',
- 'Category management' => 'Управление категориями',
- 'User management' => 'Управление пользователями',
- 'Active tasks' => 'Активные задачи',
- 'Disable public access' => 'Отключить общий доступ',
- 'Enable public access' => 'Включить общий доступ',
- 'Active projects' => 'Активные проекты',
- 'Inactive projects' => 'Неактивные проекты',
- 'Public access disabled' => 'Общий доступ отключен',
- 'Do you really want to disable this project: "%s"?' => 'Вы точно хотите отключить проект: "%s"?',
- 'Do you really want to duplicate 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' => 'Выключены',
- 'Google account linked' => 'Профиль Google связан',
- 'Github account linked' => 'Профиль GitHub связан',
- 'Username:' => 'Имя пользователя:',
- 'Name:' => 'Имя:',
- 'Email:' => 'Email:',
- 'Default project:' => 'Проект по умолчанию:',
- '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.' => 'Не удалось сменить пароль.',
- 'Change category for the task "%s"' => 'Сменить категорию для задачи "%s"',
- 'Change category' => 'Смена категории',
- '%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s обновил задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s открыл задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s перместил задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> на позицию #%d в колонке "%s"',
- '%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"' => '%s переместил задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> в колонку "%s"',
- '%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s создал задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s закрыл задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s создал подзадачу для задачи <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s обновил подзадачу для задачи <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '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 <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s обновил комментарий к задаче <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>' => '%s прокомментировал задачу <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>',
- '%s\'s activity' => '%s активность',
- 'No activity.' => 'Нет активности',
- 'RSS feed' => 'RSS лента',
- '%s updated a comment on the task #%d' => '%s обновил комментарий задачи #%d',
- '%s commented on the task #%d' => '%s откомментировал задачу #%d',
- '%s updated a subtask for the task #%d' => '%s обновил подзадачу задачи #%d',
- '%s created a subtask for the task #%d' => '%s создал подзадачу для задачи #%d',
- '%s updated the task #%d' => '%s обновил задачу #%d',
- '%s created the task #%d' => '%s создал задачу #%d',
- '%s closed the task #%d' => '%s закрыл задачу #%d',
- '%s open the task #%d' => '%s открыл задачу #%d',
- '%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"',
- '%s moved the task #%d to the position %d in the column "%s"' => '%s переместил задачу #%d на позицию %d в колонке "%s"',
- 'Activity' => 'Активность',
- 'Default values are "%s"' => 'Колонки по умолчанию: "%s"',
- 'Default columns for new projects (Comma-separated)' => 'Колонки по умолчанию для новых проектов (разделять запятой)',
- 'Task assignee change' => 'Изменен назначенный',
- '%s change the assignee of the task #%d to %s' => '%s сменил назначенного для задачи #%d на %s',
- '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s' => '%s сменил назначенного для задачи <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> на %s',
- '[%s][Column Change] %s (#%d)' => '[%s][Изменение колонки] %s (#%d)',
- '[%s][Position Change] %s (#%d)' => '[%s][Изменение позиции] %s (#%d)',
- '[%s][Assignee Change] %s (#%d)' => '[%s][Изменение назначеного] %s (#%d)',
- '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' => 'Информация',
- 'Database driver:' => 'Драйвер базы данных',
- 'Board settings' => 'Настройки доски',
- 'URL and token' => 'URL и токен',
- 'Webhook settings' => 'Параметры Webhook',
- 'URL for task creation:' => 'URL для создания задачи:',
- 'Reset token' => 'Перезагрузить токен',
- 'API endpoint:' => 'API endpoint:',
- 'Refresh interval for private board' => 'Период обновления для частных досок',
- 'Refresh interval for public board' => 'Период обновления для публичных досок',
- 'Task highlight period' => 'Время подсвечивания задачи',
- 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Период (в секундах) в течении которого задача считается недавно измененной (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' => '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"',
- 'New private project' => 'Новый проект с ограниченным доступом',
- '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.' => 'Вам ничего не назначено',
- 'My tasks' => 'Мои задачи',
- 'Activity stream' => 'Текущая активность',
- 'Dashboard' => 'Инфопанель',
- 'Confirmation' => 'Подтверждение пароля',
- // 'Allow everybody to access to this project' => '',
- // 'Everybody have access to this project.' => '',
-);
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
index 9a6866d3..8cfc7120 100644
--- a/app/Model/Acl.php
+++ b/app/Model/Acl.php
@@ -3,7 +3,7 @@
namespace Model;
/**
- * Acl model
+ * Access List
*
* @package model
* @author Frederic Guillot
@@ -16,149 +16,195 @@ class Acl extends Base
* @access private
* @var array
*/
- private $public_actions = array(
- 'user' => array('login', 'check', 'google', 'github'),
+ private $public_acl = array(
+ 'auth' => array('login', 'check'),
+ 'user' => array('google', 'github'),
'task' => array('readonly'),
'board' => array('readonly'),
'project' => array('feed'),
- 'webhook' => array('task', 'github'),
+ 'webhook' => '*',
+ 'app' => array('colors'),
+ 'ical' => '*',
);
/**
- * Controllers and actions allowed for regular users
+ * Controllers and actions for project members
*
* @access private
* @var array
*/
- private $user_actions = array(
- 'app' => array('index'),
- 'board' => array('index', 'show', 'save', 'check', 'changeassignee', 'updateassignee', 'changecategory', 'updatecategory', 'movecolumn', 'edit', 'update', 'add', 'confirm', 'remove'),
- 'project' => array('index', 'show', 'export', 'share', 'edit', 'update', 'users', 'remove', 'duplicate', 'disable', 'enable', 'activity', 'search', 'tasks', 'create', 'save'),
- 'user' => array('edit', 'forbidden', 'logout', 'show', 'external', 'unlinkgoogle', 'unlinkgithub', 'sessions', 'removesession', 'last', 'notifications', 'password'),
- 'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'),
- 'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'),
- 'subtask' => array('create', 'save', 'edit', 'update', 'confirm', 'remove', 'togglestatus'),
- 'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'open', 'duplicate', 'remove', 'description', 'move', 'copy', 'time'),
- 'category' => array('index', 'save', 'edit', 'update', 'confirm', 'remove'),
- 'action' => array('index', 'event', 'params', 'create', 'confirm', 'remove'),
+ private $member_acl = array(
+ 'board' => '*',
+ 'comment' => '*',
+ 'file' => '*',
+ 'project' => array('show'),
+ 'projectinfo' => array('tasks', 'search', 'activity'),
+ 'subtask' => '*',
+ 'task' => '*',
+ 'tasklink' => '*',
+ 'calendar' => array('show', 'project'),
);
/**
- * Return true if the specified controller/action is allowed according to the given acl
+ * Controllers and actions for project managers
*
- * @access public
- * @param array $acl Acl list
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
+ * @access private
+ * @var array
*/
- public function isAllowedAction(array $acl, $controller, $action)
- {
- if (isset($acl[$controller])) {
- return in_array($action, $acl[$controller]);
- }
+ private $manager_acl = array(
+ 'action' => '*',
+ 'analytic' => '*',
+ 'category' => '*',
+ 'column' => '*',
+ 'export' => array('tasks', 'subtasks', 'summary'),
+ 'project' => array('edit', 'update', 'share', 'integration', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'),
+ 'swimlane' => '*',
+ 'budget' => '*',
+ );
- return false;
- }
+ /**
+ * Controllers and actions for admins
+ *
+ * @access private
+ * @var array
+ */
+ private $admin_acl = array(
+ 'app' => array('dashboard'),
+ 'user' => array('index', 'create', 'save', 'remove'),
+ 'config' => '*',
+ 'link' => '*',
+ 'project' => array('remove'),
+ 'hourlyrate' => '*',
+ 'currency' => '*',
+ 'twofactor' => array('disable'),
+ );
/**
- * Return true if the given action is public
+ * 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 isPublicAction($controller, $action)
+ public function matchAcl(array $acl, $controller, $action)
{
- return $this->isAllowedAction($this->public_actions, $controller, $action);
+ $action = strtolower($action);
+ return isset($acl[$controller]) && $this->hasAction($action, $acl[$controller]);
}
/**
- * Return true if the given action is allowed for a regular user
+ * Return true if the specified action is inside the list of actions
*
* @access public
- * @param string $controller Controller name
* @param string $action Action name
+ * @param mixed $action Actions list
* @return bool
*/
- public function isUserAction($controller, $action)
+ public function hasAction($action, $actions)
{
- return $this->isAllowedAction($this->user_actions, $controller, $action);
+ if (is_array($actions)) {
+ return in_array($action, $actions);
+ }
+
+ return $actions === '*';
}
/**
- * Return true if the logged user is admin
+ * Return true if the given action is public
*
* @access public
+ * @param string $controller Controller name
+ * @param string $action Action name
* @return bool
*/
- public function isAdminUser()
+ public function isPublicAction($controller, $action)
{
- return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === true;
+ return $this->matchAcl($this->public_acl, $controller, $action);
}
/**
- * Return true if the logged user is not admin
+ * 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 isRegularUser()
+ public function isAdminAction($controller, $action)
{
- return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === false;
+ return $this->matchAcl($this->admin_acl, $controller, $action);
}
/**
- * Get the connected user id
+ * Return true if the given action is for project managers
*
* @access public
- * @return integer
+ * @param string $controller Controller name
+ * @param string $action Action name
+ * @return bool
*/
- public function getUserId()
+ public function isManagerAction($controller, $action)
{
- return isset($_SESSION['user']['id']) ? (int) $_SESSION['user']['id'] : 0;
+ return $this->matchAcl($this->manager_acl, $controller, $action);
}
/**
- * Check is the user is connected
+ * 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 isLogged()
+ public function isMemberAction($controller, $action)
{
- return ! empty($_SESSION['user']);
+ return $this->matchAcl($this->member_acl, $controller, $action);
}
/**
- * Check is the user was authenticated with the RememberMe or set the value
+ * Return true if the visitor is allowed to access to the given page
+ * We suppose the user already authenticated
*
* @access public
- * @param bool $value Set true if the user use the RememberMe
+ * @param string $controller Controller name
+ * @param string $action Action name
+ * @param integer $project_id Project id
* @return bool
*/
- public function isRememberMe($value = null)
+ public function isAllowed($controller, $action, $project_id = 0)
{
- if ($value !== null) {
- $_SESSION['is_remember_me'] = $value;
+ // If you are admin you have access to everything
+ if ($this->userSession->isAdmin()) {
+ return true;
}
- return empty($_SESSION['is_remember_me']) ? false : $_SESSION['is_remember_me'];
+ // If you access to an admin action, your are not allowed
+ if ($this->isAdminAction($controller, $action)) {
+ return false;
+ }
+
+ // Check project manager permissions
+ if ($this->isManagerAction($controller, $action)) {
+ return $this->isManagerActionAllowed($project_id);
+ }
+
+ // Check project member permissions
+ if ($this->isMemberAction($controller, $action)) {
+ return $project_id > 0 && $this->projectPermission->isMember($project_id, $this->userSession->getId());
+ }
+
+ // Other applications actions are allowed
+ return true;
}
- /**
- * Check if an action is allowed for the logged user
- *
- * @access public
- * @param string $controller Controller name
- * @param string $action Action name
- * @return bool
- */
- public function isPageAccessAllowed($controller, $action)
+ public function isManagerActionAllowed($project_id)
{
- return $this->isPublicAction($controller, $action) ||
- $this->isAdminUser() ||
- ($this->isRegularUser() && $this->isUserAction($controller, $action));
+ if ($this->userSession->isAdmin()) {
+ return true;
+ }
+
+ return $project_id > 0 && $this->projectPermission->isManager($project_id, $this->userSession->getId());
}
}
diff --git a/app/Model/Action.php b/app/Model/Action.php
index 56a1a2bb..3e8aa091 100644
--- a/app/Model/Action.php
+++ b/app/Model/Action.php
@@ -2,7 +2,9 @@
namespace Model;
-use LogicException;
+use Integration\GitlabWebhook;
+use Integration\GithubWebhook;
+use Integration\BitbucketWebhook;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
@@ -43,12 +45,18 @@ class Action extends Base
'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 logging 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'),
);
asort($values);
@@ -78,6 +86,11 @@ class Action extends Base
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'),
+ BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'),
);
asort($values);
@@ -134,9 +147,17 @@ 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'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll();
+
+ $action['params'] = array();
+
+ foreach ($params as $param) {
+ if ($param['action_id'] === $action['id']) {
+ $action['params'][] = $param;
+ }
+ }
}
return $actions;
@@ -185,6 +206,7 @@ class Action extends Base
*/
public function remove($action_id)
{
+ // $this->container['fileCache']->remove('proxy_action_getAll');
return $this->db->table(self::TABLE)->eq('id', $action_id)->remove();
}
@@ -193,7 +215,7 @@ class Action extends Base
*
* @access public
* @param array $values Required parameters to save an action
- * @return bool Success or not
+ * @return boolean|integer
*/
public function create(array $values)
{
@@ -228,7 +250,9 @@ class Action extends Base
$this->db->closeTransaction();
- return true;
+ // $this->container['fileCache']->remove('proxy_action_getAll');
+
+ return $action_id;
}
/**
@@ -238,7 +262,10 @@ class Action extends Base
*/
public function attachEvents()
{
- foreach ($this->getAll() as $action) {
+ //$actions = $this->container['fileCache']->proxy('action', 'getAll');
+ $actions = $this->getAll();
+
+ foreach ($actions as $action) {
$listener = $this->load($action['action_name'], $action['project_id'], $action['event_name']);
@@ -246,7 +273,7 @@ class Action extends Base
$listener->setParam($param['name'], $param['value']);
}
- $this->event->attach($action['event_name'], $listener);
+ $this->container['dispatcher']->addListener($action['event_name'], array($listener, 'execute'));
}
}
@@ -262,7 +289,7 @@ class Action extends Base
public function load($name, $project_id, $event)
{
$className = '\Action\\'.$name;
- return new $className($this->registry, $project_id, $event);
+ return new $className($this->container, $project_id, $event);
}
/**
@@ -301,6 +328,8 @@ class Action extends Base
}
}
+ // $this->container['fileCache']->remove('proxy_action_getAll');
+
return true;
}
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
index b9ebcfe2..86c1c43f 100644
--- a/app/Model/Authentication.php
+++ b/app/Model/Authentication.php
@@ -3,7 +3,6 @@
namespace Model;
use Core\Request;
-use Auth\Database;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
@@ -24,31 +23,31 @@ class Authentication extends Base
*/
public function backend($name)
{
- if (! isset($this->registry->$name)) {
+ if (! isset($this->container[$name])) {
$class = '\Auth\\'.ucfirst($name);
- $this->registry->$name = new $class($this->registry);
+ $this->container[$name] = new $class($this->container);
}
- return $this->registry->shared($name);
+ return $this->container[$name];
}
/**
* Check if the current user is authenticated
*
* @access public
- * @param string $controller Controller
- * @param string $action Action name
* @return bool
*/
- public function isAuthenticated($controller, $action)
+ public function isAuthenticated()
{
- // If the action is public we don't need to do any checks
- if ($this->acl->isPublicAction($controller, $action)) {
- return true;
- }
-
// If the user is already logged it's ok
- if ($this->acl->isLogged()) {
+ if ($this->userSession->isLogged()) {
+
+ // Check if the user session match an existing user
+ if (! $this->user->exists($this->userSession->getId())) {
+ $this->backend('rememberMe')->destroy($this->userSession->getId());
+ $this->session->close();
+ return false;
+ }
// We update each time the RememberMe cookie tokens
if ($this->backend('rememberMe')->hasCookie()) {
@@ -118,7 +117,7 @@ class Authentication extends Base
if (! empty($values['remember_me'])) {
$credentials = $this->backend('rememberMe')
- ->create($this->acl->getUserId(), Request::getIpAddress(), Request::getUserAgent());
+ ->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent());
$this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 72d91c3c..51ae782d 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -2,44 +2,15 @@
namespace Model;
-use Core\Event;
-use Core\Tool;
-use Core\Registry;
-use PicoDb\Database;
+use Pimple\Container;
/**
* Base model class
*
* @package model
* @author Frederic Guillot
- *
- * @property \Model\Acl $acl
- * @property \Model\Action $action
- * @property \Model\Authentication $authentication
- * @property \Model\Board $board
- * @property \Model\Category $category
- * @property \Model\Comment $comment
- * @property \Model\CommentHistory $commentHistory
- * @property \Model\Color $color
- * @property \Model\Config $config
- * @property \Model\DateParser $dateParser
- * @property \Model\File $file
- * @property \Model\LastLogin $lastLogin
- * @property \Model\Notification $notification
- * @property \Model\Project $project
- * @property \Model\ProjectPermission $projectPermission
- * @property \Model\SubTask $subTask
- * @property \Model\SubtaskHistory $subtaskHistory
- * @property \Model\Task $task
- * @property \Model\TaskExport $taskExport
- * @property \Model\TaskFinder $taskFinder
- * @property \Model\TaskHistory $taskHistory
- * @property \Model\TaskValidator $taskValidator
- * @property \Model\TimeTracking $timeTracking
- * @property \Model\User $user
- * @property \Model\Webhook $webhook
*/
-abstract class Base
+abstract class Base extends \Core\Base
{
/**
* Database instance
@@ -50,44 +21,35 @@ abstract class Base
protected $db;
/**
- * Event dispatcher instance
- *
- * @access public
- * @var \Core\Event
- */
- public $event;
-
- /**
- * Registry instance
- *
- * @access protected
- * @var \Core\Registry
- */
- protected $registry;
-
- /**
* Constructor
*
* @access public
- * @param \Core\Registry $registry Registry instance
+ * @param \Pimple\Container $container
*/
- public function __construct(Registry $registry)
+ public function __construct(Container $container)
{
- $this->registry = $registry;
- $this->db = $this->registry->shared('db');
- $this->event = $this->registry->shared('event');
+ $this->container = $container;
+ $this->db = $this->container['db'];
}
/**
- * Load automatically models
+ * Save a record in the database
*
* @access public
- * @param string $name Model name
- * @return mixed
+ * @param string $table Table name
+ * @param array $values Form values
+ * @return boolean|integer
*/
- public function __get($name)
+ public function persist($table, array $values)
{
- return Tool::loadModel($this->registry, $name);
+ return $this->db->transaction(function($db) use ($table, $values) {
+
+ if (! $db->table($table)->save($values)) {
+ return false;
+ }
+
+ return (int) $db->getConnection()->getLastId();
+ });
}
/**
@@ -95,7 +57,7 @@ abstract class Base
*
* @access public
* @param array $values Input array
- * @param array $keys List of keys to remove
+ * @param string[] $keys List of keys to remove
*/
public function removeFields(array &$values, array $keys)
{
@@ -110,8 +72,8 @@ abstract class Base
* Force some fields to be at 0 if empty
*
* @access public
- * @param array $values Input array
- * @param array $keys List of keys
+ * @param array $values Input array
+ * @param string[] $keys List of keys
*/
public function resetFields(array &$values, array $keys)
{
@@ -126,8 +88,8 @@ abstract class Base
* Force some fields to be integer
*
* @access public
- * @param array $values Input array
- * @param array $keys List of keys
+ * @param array $values Input array
+ * @param string[] $keys List of keys
*/
public function convertIntegerFields(array &$values, array $keys)
{
@@ -137,4 +99,67 @@ abstract class Base
}
}
}
+
+ /**
+ * Build SQL condition for a given time range
+ *
+ * @access protected
+ * @param string $start_time Start timestamp
+ * @param string $end_time End timestamp
+ * @param string $start_column Start column name
+ * @param string $end_column End column name
+ * @return string
+ */
+ protected function getCalendarCondition($start_time, $end_time, $start_column, $end_column)
+ {
+ $start_column = $this->db->escapeIdentifier($start_column);
+ $end_column = $this->db->escapeIdentifier($end_column);
+
+ $conditions = array(
+ "($start_column >= '$start_time' AND $start_column <= '$end_time')",
+ "($start_column <= '$start_time' AND $end_column >= '$start_time')",
+ "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))",
+ );
+
+ return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')';
+ }
+
+ /**
+ * Get common properties for task calendar events
+ *
+ * @access protected
+ * @param array $task
+ * @return array
+ */
+ protected function getTaskCalendarProperties(array &$task)
+ {
+ return array(
+ 'timezoneParam' => $this->config->getCurrentTimezone(),
+ 'id' => $task['id'],
+ 'title' => t('#%d', $task['id']).' '.$task['title'],
+ 'backgroundColor' => $this->color->getBackgroundColor($task['color_id']),
+ 'borderColor' => $this->color->getBorderColor($task['color_id']),
+ 'textColor' => 'black',
+ 'url' => $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
+ );
+ }
+
+ /**
+ * Group a collection of records by a column
+ *
+ * @access public
+ * @param array $collection
+ * @param string $column
+ * @return array
+ */
+ public function groupByColumn(array $collection, $column)
+ {
+ $result = array();
+
+ foreach ($collection as $item) {
+ $result[$item[$column]][] = $item;
+ }
+
+ return $result;
+ }
}
diff --git a/app/Model/Board.php b/app/Model/Board.php
index 4c78b0f6..eecbc91c 100644
--- a/app/Model/Board.php
+++ b/app/Model/Board.php
@@ -24,7 +24,7 @@ class Board extends Base
* Get Kanboard default columns
*
* @access public
- * @return array
+ * @return string[]
*/
public function getDefaultColumns()
{
@@ -47,7 +47,7 @@ class Board extends Base
$column_name = trim($column_name);
if (! empty($column_name)) {
- $columns[] = array('title' => $column_name, 'task_limit' => 0);
+ $columns[] = array('title' => $column_name, 'task_limit' => 0, 'description' => '');
}
}
@@ -73,6 +73,7 @@ class Board extends Base
'position' => ++$position,
'project_id' => $project_id,
'task_limit' => $column['task_limit'],
+ 'description' => $column['description'],
);
if (! $this->db->table(self::TABLE)->save($values)) {
@@ -94,7 +95,7 @@ class Board extends Base
public function duplicate($project_from, $project_to)
{
$columns = $this->db->table(Board::TABLE)
- ->columns('title', 'task_limit')
+ ->columns('title', 'task_limit', 'description')
->eq('project_id', $project_from)
->asc('position')
->findAll();
@@ -109,61 +110,79 @@ class Board extends Base
* @param integer $project_id Project id
* @param string $title Column title
* @param integer $task_limit Task limit
- * @return boolean
+ * @param string $description Column description
+ * @return boolean|integer
*/
- public function addColumn($project_id, $title, $task_limit = 0)
+ public function addColumn($project_id, $title, $task_limit = 0, $description = '')
{
- return $this->db->table(self::TABLE)->save(array(
+ $values = array(
'project_id' => $project_id,
'title' => $title,
- 'task_limit' => $task_limit,
+ 'task_limit' => intval($task_limit),
'position' => $this->getLastColumnPosition($project_id) + 1,
- ));
+ 'description' => $description,
+ );
+
+ return $this->persist(self::TABLE, $values);
}
/**
- * Update columns
+ * Update a column
*
* @access public
- * @param array $values Form values
+ * @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(array $values)
+ public function updateColumn($column_id, $title, $task_limit = 0, $description = '')
{
- $columns = array();
-
- foreach (array('title', 'task_limit') as $field) {
- foreach ($values[$field] as $column_id => $value) {
- $columns[$column_id][$field] = $value;
- }
- }
+ return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
+ 'title' => $title,
+ 'task_limit' => intval($task_limit),
+ 'description' => $description,
+ ));
+ }
- $this->db->startTransaction();
+ /**
+ * 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 => $values) {
- $this->updateColumn($column_id, $values['title'], (int) $values['task_limit']);
+ foreach ($columns as $column_id => $column_position) {
+ $columns[$column_id] = $position++;
}
- $this->db->closeTransaction();
-
- return true;
+ return $columns;
}
/**
- * Update a column
+ * Save the new positions for a set of columns
*
* @access public
- * @param integer $column_id Column id
- * @param string $title Column title
- * @param integer $task_limit Task limit
+ * @param array $columns Hashmap of column_id/column_position
* @return boolean
*/
- public function updateColumn($column_id, $title, $task_limit = 0)
+ public function saveColumnPositions(array $columns)
{
- return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array(
- 'title' => $title,
- 'task_limit' => $task_limit,
- ));
+ return $this->db->transaction(function ($db) use ($columns) {
+
+ foreach ($columns as $column_id => $position) {
+ if (! $db->table(Board::TABLE)->eq('id', $column_id)->update(array('position' => $position))) {
+ return false;
+ }
+ }
+ });
}
/**
@@ -176,7 +195,7 @@ class Board extends Base
*/
public function moveDown($project_id, $column_id)
{
- $columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position');
+ $columns = $this->getNormalizedColumnPositions($project_id);
$positions = array_flip($columns);
if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) {
@@ -184,12 +203,7 @@ class Board extends Base
$position = ++$columns[$column_id];
$columns[$positions[$position]]--;
- $this->db->startTransaction();
- $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position));
- $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $columns[$positions[$position]]));
- $this->db->closeTransaction();
-
- return true;
+ return $this->saveColumnPositions($columns);
}
return false;
@@ -205,7 +219,7 @@ class Board extends Base
*/
public function moveUp($project_id, $column_id)
{
- $columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position');
+ $columns = $this->getNormalizedColumnPositions($project_id);
$positions = array_flip($columns);
if (isset($columns[$column_id]) && $columns[$column_id] > 1) {
@@ -213,42 +227,79 @@ class Board extends Base
$position = --$columns[$column_id];
$columns[$positions[$position]]++;
- $this->db->startTransaction();
- $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position));
- $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $columns[$positions[$position]]));
- $this->db->closeTransaction();
-
- return true;
+ return $this->saveColumnPositions($columns);
}
return false;
}
/**
- * Get all columns and tasks for a given project
+ * Get all tasks sorted by columns and swimlanes
*
* @access public
* @param integer $project_id Project id
- * @param array $filters
* @return array
*/
- public function get($project_id, array $filters = array())
+ public function getBoard($project_id)
{
+ $swimlanes = $this->swimlane->getSwimlanes($project_id);
$columns = $this->getColumns($project_id);
- $tasks = $this->taskFinder->getTasksOnBoard($project_id);
+ $nb_columns = count($columns);
- foreach ($columns as &$column) {
+ for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) {
- $column['tasks'] = array();
+ $swimlanes[$i]['columns'] = $columns;
+ $swimlanes[$i]['nb_columns'] = $nb_columns;
+ $swimlanes[$i]['nb_tasks'] = 0;
- foreach ($tasks as &$task) {
- if ($task['column_id'] == $column['id']) {
- $column['tasks'][] = $task;
- }
+ for ($j = 0; $j < $nb_columns; $j++) {
+ $swimlanes[$i]['columns'][$j]['tasks'] = $this->taskFinder->getTasksByColumnAndSwimlane($project_id, $columns[$j]['id'], $swimlanes[$i]['id']);
+ $swimlanes[$i]['columns'][$j]['nb_tasks'] = count($swimlanes[$i]['columns'][$j]['tasks']);
+ $swimlanes[$i]['columns'][$j]['score'] = $this->getColumnSum($swimlanes[$i]['columns'][$j]['tasks'], 'score');
+ $swimlanes[$i]['nb_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks'];
}
}
- return $columns;
+ return $swimlanes;
+ }
+
+ /**
+ * Calculate the sum of the defined field for a list of tasks
+ *
+ * @access public
+ * @param array $tasks
+ * @param string $field
+ * @return integer
+ */
+ public function getColumnSum(array &$tasks, $field)
+ {
+ $sum = 0;
+
+ foreach ($tasks as $task) {
+ $sum += $task['score'];
+ }
+
+ return $sum;
+ }
+
+ /**
+ * Get the total of tasks per column
+ *
+ * @access public
+ * @param integer $project_id
+ * @param boolean $prepend Prepend default value
+ * @return array
+ */
+ public function getColumnStats($project_id, $prepend = false)
+ {
+ $listing = $this->db
+ ->hashtable(Task::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', 1)
+ ->groupBy('column_id')
+ ->getAll('column_id', 'COUNT(*) AS total');
+
+ return $prepend ? array(-1 => t('All columns')) + $listing : $listing;
}
/**
@@ -264,15 +315,29 @@ class Board extends Base
}
/**
+ * 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)
+ public function getColumnsList($project_id, $prepend = false)
{
- return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'title');
+ $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;
}
/**
@@ -343,22 +408,16 @@ class Board extends Base
* Validate column modification
*
* @access public
- * @param array $columns Original columns List
* @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 $columns, array $values)
+ public function validateModification(array $values)
{
- $rules = array();
-
- foreach ($columns as $column_id => $column_title) {
- $rules[] = new Validators\Integer('task_limit['.$column_id.']', t('This value must be an integer'));
- $rules[] = new Validators\GreaterThan('task_limit['.$column_id.']', t('This value must be greater than %d', 0), 0);
- $rules[] = new Validators\Required('title['.$column_id.']', t('The title is required'));
- $rules[] = new Validators\MaxLength('title['.$column_id.']', t('The maximum length is %d characters', 50), 50);
- }
-
- $v = new Validator($values, $rules);
+ $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(),
diff --git a/app/Model/Budget.php b/app/Model/Budget.php
new file mode 100644
index 00000000..d74dd870
--- /dev/null
+++ b/app/Model/Budget.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace Model;
+
+use DateInterval;
+use DateTime;
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Budget
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Budget extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'budget_lines';
+
+ /**
+ * Get all budget lines for a project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAll($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('date')->findAll();
+ }
+
+ /**
+ * Get the current total of the budget
+ *
+ * @access public
+ * @param integer $project_id
+ * @return float
+ */
+ public function getTotal($project_id)
+ {
+ $result = $this->db->table(self::TABLE)->columns('SUM(amount) as total')->eq('project_id', $project_id)->findOne();
+ return isset($result['total']) ? (float) $result['total'] : 0;
+ }
+
+ /**
+ * Get breakdown by tasks/subtasks/users
+ *
+ * @access public
+ * @param integer $project_id
+ * @return \PicoDb\Table
+ */
+ public function getSubtaskBreakdown($project_id)
+ {
+ return $this->db
+ ->table(SubtaskTimeTracking::TABLE)
+ ->columns(
+ SubtaskTimeTracking::TABLE.'.id',
+ SubtaskTimeTracking::TABLE.'.user_id',
+ SubtaskTimeTracking::TABLE.'.subtask_id',
+ SubtaskTimeTracking::TABLE.'.start',
+ SubtaskTimeTracking::TABLE.'.time_spent',
+ Subtask::TABLE.'.task_id',
+ Subtask::TABLE.'.title AS subtask_title',
+ Task::TABLE.'.title AS task_title',
+ Task::TABLE.'.project_id',
+ User::TABLE.'.username',
+ User::TABLE.'.name'
+ )
+ ->join(Subtask::TABLE, 'id', 'subtask_id')
+ ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq(Task::TABLE.'.project_id', $project_id)
+ ->filter(array($this, 'applyUserRate'));
+ }
+
+ /**
+ * Gather necessary information to display the budget graph
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getDailyBudgetBreakdown($project_id)
+ {
+ $out = array();
+ $in = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->gt('amount', 0)->asc('date')->getAll('date', 'amount');
+ $time_slots = $this->getSubtaskBreakdown($project_id)->findAll();
+
+ foreach ($time_slots as $slot) {
+ $date = date('Y-m-d', $slot['start']);
+
+ if (! isset($out[$date])) {
+ $out[$date] = 0;
+ }
+
+ $out[$date] += $slot['cost'];
+ }
+
+ $start = key($in) ?: key($out);
+ $end = new DateTime;
+ $left = 0;
+ $serie = array();
+
+ for ($today = new DateTime($start); $today <= $end; $today->add(new DateInterval('P1D'))) {
+
+ $date = $today->format('Y-m-d');
+ $today_in = isset($in[$date]) ? (int) $in[$date] : 0;
+ $today_out = isset($out[$date]) ? (int) $out[$date] : 0;
+
+ if ($today_in > 0 || $today_out > 0) {
+
+ $left += $today_in;
+ $left -= $today_out;
+
+ $serie[] = array(
+ 'date' => $date,
+ 'in' => $today_in,
+ 'out' => -$today_out,
+ 'left' => $left,
+ );
+ }
+ }
+
+ return $serie;
+ }
+
+ /**
+ * Filter callback to apply the rate according to the effective date
+ *
+ * @access public
+ * @param array $records
+ * @return array
+ */
+ public function applyUserRate(array $records)
+ {
+ $rates = $this->hourlyRate->getAllByProject($records[0]['project_id']);
+
+ foreach ($records as &$record) {
+
+ $hourly_price = 0;
+
+ foreach ($rates as $rate) {
+
+ if ($rate['user_id'] == $record['user_id'] && date('Y-m-d', $rate['date_effective']) <= date('Y-m-d', $record['start'])) {
+ $hourly_price = $this->currency->getPrice($rate['currency'], $rate['rate']);
+ break;
+ }
+ }
+
+ $record['cost'] = $hourly_price * $record['time_spent'];
+ }
+
+ return $records;
+ }
+
+ /**
+ * Add a new budget line in the database
+ *
+ * @access public
+ * @param integer $project_id
+ * @param float $amount
+ * @param string $comment
+ * @param string $date
+ * @return boolean|integer
+ */
+ public function create($project_id, $amount, $comment, $date = '')
+ {
+ $values = array(
+ 'project_id' => $project_id,
+ 'amount' => $amount,
+ 'comment' => $comment,
+ 'date' => $date ?: date('Y-m-d'),
+ );
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Remove a specific budget line
+ *
+ * @access public
+ * @param integer $budget_id
+ * @return boolean
+ */
+ public function remove($budget_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $budget_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('project_id', t('Field required')),
+ new Validators\Required('amount', t('Field required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+} \ No newline at end of file
diff --git a/app/Model/Category.php b/app/Model/Category.php
index fb54594b..c8ba7251 100644
--- a/app/Model/Category.php
+++ b/app/Model/Category.php
@@ -46,6 +46,66 @@ class Category extends Base
}
/**
+ * Get the category name by the id
+ *
+ * @access public
+ * @param integer $category_id Category id
+ * @return string
+ */
+ public function getNameById($category_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('name') ?: '';
+ }
+
+ /**
+ * Get a category id by the project and the name
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $category_name Category name
+ * @return integer
+ */
+ public function getIdByName($project_id, $category_name)
+ {
+ return (int) $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('name', $category_name)
+ ->findOneColumn('id');
+ }
+
+ /**
+ * Prepare categories to be displayed on the board
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getBoardCategories($project_id)
+ {
+ $descriptions = array();
+
+ $listing = array(
+ -1 => t('All categories'),
+ 0 => t('No category'),
+ );
+
+ $categories = $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->asc('name')
+ ->findAll();
+
+ foreach ($categories as $category) {
+ $listing[$category['id']] = $category['name'];
+ $descriptions[$category['id']] = $category['description'];
+ }
+
+ return array(
+ $listing,
+ $descriptions,
+ );
+ }
+
+ /**
* Return the list of all categories
*
* @access public
@@ -56,10 +116,10 @@ class Category extends Base
*/
public function getList($project_id, $prepend_none = true, $prepend_all = false)
{
- $listing = $this->db->table(self::TABLE)
+ $listing = $this->db->hashtable(self::TABLE)
->eq('project_id', $project_id)
->asc('name')
- ->listing('id', 'name');
+ ->getAll('id', 'name');
$prepend = array();
@@ -90,15 +150,38 @@ class Category extends Base
}
/**
- * Create a category
+ * Create default cetegories during project creation (transaction already started in Project::create())
+ *
+ * @access public
+ * @param integer $project_id
+ */
+ public function createDefaultCategories($project_id)
+ {
+ $categories = explode(',', $this->config->get('project_categories'));
+
+ foreach ($categories as $category) {
+
+ $category = trim($category);
+
+ if (! empty($category)) {
+ $this->db->table(self::TABLE)->insert(array(
+ 'project_id' => $project_id,
+ 'name' => $category,
+ ));
+ }
+ }
+ }
+
+ /**
+ * Create a category (run inside a transaction)
*
* @access public
* @param array $values Form values
- * @return bool
+ * @return bool|integer
*/
public function create(array $values)
{
- return $this->db->table(self::TABLE)->save($values);
+ return $this->persist(self::TABLE, $values);
}
/**
@@ -137,26 +220,26 @@ class Category extends Base
}
/**
- * Duplicate categories from a project to another one
+ * Duplicate categories from a project to another one, must be executed inside a transaction
*
* @author Antonio Rabelo
- * @param integer $project_from Project Template
- * @return integer $project_to Project that receives the copy
+ * @param integer $src_project_id Source project id
+ * @return integer $dst_project_id Destination project id
* @return boolean
*/
- public function duplicate($project_from, $project_to)
+ public function duplicate($src_project_id, $dst_project_id)
{
$categories = $this->db->table(self::TABLE)
->columns('name')
- ->eq('project_id', $project_from)
+ ->eq('project_id', $src_project_id)
->asc('name')
->findAll();
foreach ($categories as $category) {
- $category['project_id'] = $project_to;
+ $category['project_id'] = $dst_project_id;
- if (! $this->category->create($category)) {
+ if (! $this->db->table(self::TABLE)->save($category)) {
return false;
}
}
diff --git a/app/Model/Color.php b/app/Model/Color.php
index f414e837..241a97c7 100644
--- a/app/Model/Color.php
+++ b/app/Model/Color.php
@@ -3,7 +3,7 @@
namespace Model;
/**
- * Color model (TODO: model for the future color picker)
+ * Color model
*
* @package model
* @author Frederic Guillot
@@ -11,14 +11,60 @@ namespace Model;
class Color extends Base
{
/**
+ * Default colors
+ *
+ * @access private
+ * @var array
+ */
+ private $default_colors = array(
+ 'yellow' => array(
+ 'name' => 'Yellow',
+ 'background' => 'rgb(245, 247, 196)',
+ 'border' => 'rgb(223, 227, 45)',
+ ),
+ 'blue' => array(
+ 'name' => 'Blue',
+ 'background' => 'rgb(219, 235, 255)',
+ 'border' => 'rgb(168, 207, 255)',
+ ),
+ 'green' => array(
+ 'name' => 'Green',
+ 'background' => 'rgb(189, 244, 203)',
+ 'border' => 'rgb(74, 227, 113)',
+ ),
+ 'purple' => array(
+ 'name' => 'Purple',
+ 'background' => 'rgb(223, 176, 255)',
+ 'border' => 'rgb(205, 133, 254)',
+ ),
+ 'red' => array(
+ 'name' => 'Red',
+ 'background' => 'rgb(255, 187, 187)',
+ 'border' => 'rgb(255, 151, 151)',
+ ),
+ 'orange' => array(
+ 'name' => 'Orange',
+ 'background' => 'rgb(255, 215, 179)',
+ 'border' => 'rgb(255, 172, 98)',
+ ),
+ 'grey' => array(
+ 'name' => 'Grey',
+ 'background' => 'rgb(238, 238, 238)',
+ 'border' => 'rgb(204, 204, 204)',
+ ),
+ );
+
+ /**
* Get available colors
*
* @access public
* @return array
*/
- public function getList()
+ public function getList($prepend = false)
{
- return array(
+ $listing = $prepend ? array('' => t('All colors')) : array();
+
+ return $listing + array(
'yellow' => t('Yellow'),
'blue' => t('Blue'),
'green' => t('Green'),
@@ -28,4 +74,68 @@ class Color extends Base
'grey' => t('Grey'),
);
}
+
+ /**
+ * Get the default color
+ *
+ * @access public
+ * @return string
+ */
+ public function getDefaultColor()
+ {
+ return 'yellow'; // TODO: make this parameter configurable
+ }
+
+ /**
+ * Get Bordercolor from string
+ *
+ * @access public
+ * @param string $color_id Color id
+ * @return string
+ */
+ public function getBorderColor($color_id)
+ {
+ if (isset($this->default_colors[$color_id])) {
+ return $this->default_colors[$color_id]['border'];
+ }
+
+ return $this->default_colors[$this->getDefaultColor()]['border'];
+ }
+
+ /**
+ * Get background color from the color_id
+ *
+ * @access public
+ * @param string $color_id Color id
+ * @return string
+ */
+ public function getBackgroundColor($color_id)
+ {
+ if (isset($this->default_colors[$color_id])) {
+ return $this->default_colors[$color_id]['background'];
+ }
+
+ return $this->default_colors[$this->getDefaultColor()]['background'];
+ }
+
+ /**
+ * Get CSS stylesheet of all colors
+ *
+ * @access public
+ * @return string
+ */
+ public function getCss()
+ {
+ $buffer = '';
+
+ foreach ($this->default_colors as $color => $values) {
+ $buffer .= 'td.color-'.$color.',';
+ $buffer .= 'div.color-'.$color.' {';
+ $buffer .= 'background-color: '.$values['background'].';';
+ $buffer .= 'border-color: '.$values['border'];
+ $buffer .= '}';
+ }
+
+ return $buffer;
+ }
}
diff --git a/app/Model/Comment.php b/app/Model/Comment.php
index cd361b1d..3aa9c027 100644
--- a/app/Model/Comment.php
+++ b/app/Model/Comment.php
@@ -2,6 +2,7 @@
namespace Model;
+use Event\CommentEvent;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
@@ -46,7 +47,8 @@ class Comment extends Base
self::TABLE.'.user_id',
self::TABLE.'.comment',
User::TABLE.'.username',
- User::TABLE.'.name'
+ User::TABLE.'.name',
+ User::TABLE.'.email'
)
->join(User::TABLE, 'id', 'user_id')
->orderBy(self::TABLE.'.date', 'ASC')
@@ -95,24 +97,22 @@ class Comment extends Base
}
/**
- * Save a comment in the database
+ * Create a new comment
*
* @access public
* @param array $values Form values
- * @return boolean
+ * @return boolean|integer
*/
public function create(array $values)
{
$values['date'] = time();
+ $comment_id = $this->persist(self::TABLE, $values);
- if ($this->db->table(self::TABLE)->save($values)) {
-
- $values['id'] = $this->db->getConnection()->getLastId();
- $this->event->trigger(self::EVENT_CREATE, $values);
- return true;
+ if ($comment_id) {
+ $this->container['dispatcher']->dispatch(self::EVENT_CREATE, new CommentEvent(array('id' => $comment_id) + $values));
}
- return false;
+ return $comment_id;
}
/**
@@ -129,7 +129,9 @@ class Comment extends Base
->eq('id', $values['id'])
->update(array('comment' => $values['comment']));
- $this->event->trigger(self::EVENT_UPDATE, $values);
+ if ($result) {
+ $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values));
+ }
return $result;
}
diff --git a/app/Model/Config.php b/app/Model/Config.php
index 066d3993..813cc84f 100644
--- a/app/Model/Config.php
+++ b/app/Model/Config.php
@@ -2,8 +2,6 @@
namespace Model;
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
use Core\Translator;
use Core\Security;
use Core\Session;
@@ -24,41 +22,132 @@ class Config extends Base
const TABLE = 'settings';
/**
+ * 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'),
+ );
+ }
+
+ /**
* Get available timezones
*
* @access public
+ * @param boolean $prepend Prepend a default value
* @return array
*/
- public function getTimezones()
+ public function getTimezones($prepend = false)
{
$timezones = timezone_identifiers_list();
- return array_combine(array_values($timezones), $timezones);
+ $listing = array_combine(array_values($timezones), $timezones);
+
+ if ($prepend) {
+ return array('' => t('Application default')) + $listing;
+ }
+
+ return $listing;
}
/**
* Get available languages
*
* @access public
+ * @param boolean $prepend Prepend a default value
* @return array
*/
- public function getLanguages()
+ public function getLanguages($prepend = false)
{
// Sorted by value
- return array(
+ $languages = array(
'da_DK' => 'Dansk',
'de_DE' => 'Deutsch',
'en_US' => 'English',
'es_ES' => 'Español',
'fr_FR' => 'Français',
'it_IT' => 'Italiano',
+ 'hu_HU' => 'Magyar',
+ 'nl_NL' => 'Nederlands',
'pl_PL' => 'Polski',
'pt_BR' => 'Português (Brasil)',
'ru_RU' => 'Русский',
+ 'sr_Latn_RS' => 'Srpski',
'fi_FI' => 'Suomi',
'sv_SE' => 'Svenska',
+ 'tr_TR' => 'Türkçe',
'zh_CN' => '中文(简体)',
'ja_JP' => '日本語',
+ 'th_TH' => 'ไทย',
+ );
+
+ if ($prepend) {
+ return array('' => t('Application default')) + $languages;
+ }
+
+ return $languages;
+ }
+
+ /**
+ * Get javascript language code
+ *
+ * @access public
+ * @return string
+ */
+ public function getJsLanguageCode()
+ {
+ $languages = array(
+ 'da_DK' => 'da',
+ 'de_DE' => 'de',
+ 'en_US' => 'en',
+ 'es_ES' => 'es',
+ 'fr_FR' => 'fr',
+ 'it_IT' => 'it',
+ 'hu_HU' => 'hu',
+ 'nl_NL' => 'nl',
+ 'pl_PL' => 'pl',
+ 'pt_BR' => 'pt-br',
+ 'ru_RU' => 'ru',
+ 'sr_Latn_RS' => 'sr',
+ 'fi_FI' => 'fi',
+ 'sv_SE' => 'sv',
+ 'tr_TR' => 'tr',
+ 'zh_CN' => 'zh-cn',
+ 'ja_JP' => 'ja',
+ 'th_TH' => 'th',
);
+
+ $lang = $this->getCurrentLanguage();
+
+ return isset($languages[$lang]) ? $languages[$lang] : 'en';
+ }
+
+ /**
+ * Get current language
+ *
+ * @access public
+ * @return string
+ */
+ public function getCurrentLanguage()
+ {
+ if ($this->userSession->isLogged() && ! empty($this->session['user']['language'])) {
+ return $this->session['user']['language'];
+ }
+
+ return $this->get('application_language', 'en_US');
}
/**
@@ -76,12 +165,13 @@ class Config extends Base
return $value ?: $default_value;
}
- if (! isset($_SESSION['config'][$name])) {
- $_SESSION['config'] = $this->getAll();
+ // Cache config in session
+ if (! isset($this->session['config'][$name])) {
+ $this->session['config'] = $this->getAll();
}
- if (! empty($_SESSION['config'][$name])) {
- return $_SESSION['config'][$name];
+ if (! empty($this->session['config'][$name])) {
+ return $this->session['config'][$name];
}
return $default_value;
@@ -95,7 +185,7 @@ class Config extends Base
*/
public function getAll()
{
- return $this->db->table(self::TABLE)->listing('option', 'value');
+ return $this->db->hashtable(self::TABLE)->getAll('option', 'value');
}
/**
@@ -126,7 +216,7 @@ class Config extends Base
*/
public function reload()
{
- $_SESSION['config'] = $this->getAll();
+ $this->session['config'] = $this->getAll();
$this->setupTranslations();
}
@@ -137,11 +227,22 @@ class Config extends Base
*/
public function setupTranslations()
{
- $language = $this->get('application_language', 'en_US');
+ Translator::load($this->getCurrentLanguage());
+ }
- if ($language !== 'en_US') {
- Translator::load($language);
+ /**
+ * Get current timezone
+ *
+ * @access public
+ * @return string
+ */
+ public function getCurrentTimezone()
+ {
+ if ($this->userSession->isLogged() && ! empty($this->session['user']['timezone'])) {
+ return $this->session['user']['timezone'];
}
+
+ return $this->get('application_timezone', 'UTC');
}
/**
@@ -151,7 +252,7 @@ class Config extends Base
*/
public function setupTimezone()
{
- date_default_timezone_set($this->get('application_timezone', 'UTC'));
+ date_default_timezone_set($this->getCurrentTimezone());
}
/**
diff --git a/app/Model/Currency.php b/app/Model/Currency.php
new file mode 100644
index 00000000..6ae842e7
--- /dev/null
+++ b/app/Model/Currency.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Currency
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Currency extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'currencies';
+
+ /**
+ * Get all currency rates
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->db->table(self::TABLE)->findAll();
+ }
+
+ /**
+ * Calculate the price for the reference currency
+ *
+ * @access public
+ * @param string $currency
+ * @param double $price
+ * @return double
+ */
+ public function getPrice($currency, $price)
+ {
+ static $rates = null;
+ $reference = $this->config->get('application_currency', 'USD');
+
+ if ($reference !== $currency) {
+ $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : array();
+ $rate = isset($rates[$currency]) ? $rates[$currency] : 1;
+
+ return $rate * $price;
+ }
+
+ return $price;
+ }
+
+ /**
+ * Add a new currency rate
+ *
+ * @access public
+ * @param string $currency
+ * @param float $rate
+ * @return boolean|integer
+ */
+ public function create($currency, $rate)
+ {
+ if ($this->db->table(self::TABLE)->eq('currency', $currency)->count() === 1) {
+ return $this->update($currency, $rate);
+ }
+
+ return $this->persist(self::TABLE, compact('currency', 'rate'));
+ }
+
+ /**
+ * Update a currency rate
+ *
+ * @access public
+ * @param string $currency
+ * @param float $rate
+ * @return boolean
+ */
+ public function update($currency, $rate)
+ {
+ 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/DateParser.php b/app/Model/DateParser.php
index 38265f98..be79a92e 100644
--- a/app/Model/DateParser.php
+++ b/app/Model/DateParser.php
@@ -13,6 +13,47 @@ use DateTime;
class DateParser extends Base
{
/**
+ * Return true if the date is within the date range
+ *
+ * @access public
+ * @param DateTime $date
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return boolean
+ */
+ public function withinDateRange(DateTime $date, DateTime $start, DateTime $end)
+ {
+ return $date >= $start && $date <= $end;
+ }
+
+ /**
+ * Get the total number of hours between 2 datetime objects
+ * Minutes are rounded to the nearest quarter
+ *
+ * @access public
+ * @param DateTime $d1
+ * @param DateTime $d2
+ * @return float
+ */
+ public function getHours(DateTime $d1, DateTime $d2)
+ {
+ $seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp()));
+ return round($seconds / 3600, 2);
+ }
+
+ /**
+ * Round the timestamp to the nearest quarter
+ *
+ * @access public
+ * @param integer $seconds Timestamp
+ * @return integer
+ */
+ public function getRoundedSeconds($seconds)
+ {
+ return (int) round($seconds / (15 * 60)) * (15 * 60);
+ }
+
+ /**
* Return a timestamp if the given date format is correct otherwise return 0
*
* @access public
@@ -60,7 +101,7 @@ class DateParser extends Base
* Return the list of supported date formats (for the parser)
*
* @access public
- * @return array
+ * @return string[]
*/
public function getDateFormats()
{
@@ -87,23 +128,35 @@ class DateParser extends Base
}
/**
- * For a given timestamp, reset the date to midnight
+ * Remove the time from a timestamp
*
* @access public
* @param integer $timestamp Timestamp
* @return integer
*/
- public function resetDateToMidnight($timestamp)
+ public function removeTimeFromTimestamp($timestamp)
{
return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp));
}
/**
+ * Get a timetstamp from an ISO date format
+ *
+ * @access public
+ * @param string $date Date format
+ * @return integer
+ */
+ public function getTimestampFromIsoFormat($date)
+ {
+ return $this->removeTimeFromTimestamp(ctype_digit($date) ? $date : strtotime($date));
+ }
+
+ /**
* Format date (form display)
*
* @access public
* @param array $values Database values
- * @param array $fields Date fields
+ * @param string[] $fields Date fields
* @param string $format Date format
*/
public function format(array &$values, array $fields, $format = '')
@@ -128,14 +181,14 @@ class DateParser extends Base
*
* @access public
* @param array $values Database values
- * @param array $fields Date fields
+ * @param string[] $fields Date fields
*/
public function convert(array &$values, array $fields)
{
foreach ($fields as $field) {
if (! empty($values[$field]) && ! is_numeric($values[$field])) {
- $values[$field] = $this->getTimestamp($values[$field]);
+ $values[$field] = $this->removeTimeFromTimestamp($this->getTimestamp($values[$field]));
}
}
}
diff --git a/app/Model/File.php b/app/Model/File.php
index d5a0c7cd..1f62a55e 100644
--- a/app/Model/File.php
+++ b/app/Model/File.php
@@ -2,6 +2,8 @@
namespace Model;
+use Event\FileEvent;
+
/**
* File model
*
@@ -15,14 +17,7 @@ class File extends Base
*
* @var string
*/
- const TABLE = 'task_has_files';
-
- /**
- * Directory where are stored files
- *
- * @var string
- */
- const BASE_PATH = 'data/files/';
+ const TABLE = 'files';
/**
* Events
@@ -54,7 +49,7 @@ class File extends Base
{
$file = $this->getbyId($file_id);
- if (! empty($file) && @unlink(self::BASE_PATH.$file['path'])) {
+ if (! empty($file) && @unlink(FILES_DIR.$file['path'])) {
return $this->db->table(self::TABLE)->eq('id', $file_id)->remove();
}
@@ -85,17 +80,24 @@ class File extends Base
* @param string $name Filename
* @param string $path Path on the disk
* @param bool $is_image Image or not
+ * @param integer $size File size
* @return bool
*/
- public function create($task_id, $name, $path, $is_image)
+ public function create($task_id, $name, $path, $is_image, $size)
{
- $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id, 'name' => $name));
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_CREATE,
+ new FileEvent(array('task_id' => $task_id, 'name' => $name))
+ );
return $this->db->table(self::TABLE)->save(array(
'task_id' => $task_id,
- 'name' => $name,
+ 'name' => substr($name, 0, 255),
'path' => $path,
'is_image' => $is_image ? '1' : '0',
+ 'size' => $size,
+ 'user_id' => $this->userSession->getId() ?: 0,
+ 'date' => time(),
));
}
@@ -108,9 +110,83 @@ class File extends Base
*/
public function getAll($task_id)
{
- return $this->db->table(self::TABLE)
+ 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')
+ ->eq('task_id', $task_id)
+ ->asc(self::TABLE.'.name')
+ ->findAll();
+ }
+
+ /**
+ * Get all images for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getAllImages($task_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')
+ ->eq('task_id', $task_id)
+ ->eq('is_image', 1)
+ ->asc(self::TABLE.'.name')
+ ->findAll();
+ }
+
+ /**
+ * Get all files without images for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getAllDocuments($task_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')
->eq('task_id', $task_id)
- ->asc('name')
+ ->eq('is_image', 0)
+ ->asc(self::TABLE.'.name')
->findAll();
}
@@ -123,7 +199,17 @@ class File extends Base
*/
public function isImage($filename)
{
- return getimagesize($filename) !== false;
+ $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+
+ switch ($extension) {
+ case 'jpeg':
+ case 'jpg':
+ case 'png':
+ case 'gif':
+ return true;
+ }
+
+ return false;
}
/**
@@ -147,14 +233,14 @@ class File extends Base
*/
public function setup()
{
- if (! is_dir(self::BASE_PATH)) {
- if (! mkdir(self::BASE_PATH, 0755, true)) {
- die('Unable to create the upload directory: "'.self::BASE_PATH.'"');
+ if (! is_dir(FILES_DIR)) {
+ if (! mkdir(FILES_DIR, 0755, true)) {
+ die('Unable to create the upload directory: "'.FILES_DIR.'"');
}
}
- if (! is_writable(self::BASE_PATH)) {
- die('The directory "'.self::BASE_PATH.'" must be writeable by your webserver user');
+ if (! is_writable(FILES_DIR)) {
+ die('The directory "'.FILES_DIR.'" must be writeable by your webserver user');
}
}
@@ -162,9 +248,9 @@ class File extends Base
* Handle file upload
*
* @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param string $form_name File form name
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param string $form_name File form name
* @return bool
*/
public function upload($project_id, $task_id, $form_name)
@@ -182,15 +268,16 @@ class File extends Base
$uploaded_filename = $_FILES[$form_name]['tmp_name'][$key];
$destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
- @mkdir(self::BASE_PATH.dirname($destination_filename), 0755, true);
+ @mkdir(FILES_DIR.dirname($destination_filename), 0755, true);
- if (@move_uploaded_file($uploaded_filename, self::BASE_PATH.$destination_filename)) {
+ if (@move_uploaded_file($uploaded_filename, FILES_DIR.$destination_filename)) {
$result[] = $this->create(
$task_id,
$original_filename,
$destination_filename,
- $this->isImage(self::BASE_PATH.$destination_filename)
+ $this->isImage($original_filename),
+ $_FILES[$form_name]['size'][$key]
);
}
}
@@ -199,4 +286,144 @@ class File extends Base
return count(array_unique($result)) === 1;
}
+
+ /**
+ * Handle screenshot upload
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param string $blob Base64 encoded image
+ * @return bool
+ */
+ public function uploadScreenshot($project_id, $task_id, $blob)
+ {
+ $data = base64_decode($blob);
+
+ if (empty($data)) {
+ return false;
+ }
+
+ $original_filename = e('Screenshot taken %s', dt('%B %e, %Y at %k:%M %p', time())).'.png';
+ $destination_filename = $this->generatePath($project_id, $task_id, $original_filename);
+
+ @mkdir(FILES_DIR.dirname($destination_filename), 0755, true);
+ @file_put_contents(FILES_DIR.$destination_filename, $data);
+
+ return $this->create(
+ $task_id,
+ $original_filename,
+ $destination_filename,
+ true,
+ strlen($data)
+ );
+ }
+
+ /**
+ * Handle file upload (base64 encoded content)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param string $filename Filename
+ * @param bool $is_image Is image file?
+ * @param string $blob Base64 encoded image
+ * @return bool
+ */
+ public function uploadContent($project_id, $task_id, $filename, $is_image, &$blob)
+ {
+ $data = base64_decode($blob);
+
+ if (empty($data)) {
+ return false;
+ }
+
+ $destination_filename = $this->generatePath($project_id, $task_id, $filename);
+
+ @mkdir(FILES_DIR.dirname($destination_filename), 0755, true);
+ @file_put_contents(FILES_DIR.$destination_filename, $data);
+
+ return $this->create(
+ $task_id,
+ $filename,
+ $destination_filename,
+ $is_image,
+ strlen($data)
+ );
+ }
+
+ /**
+ * Generate a jpeg thumbnail from an image (output directly the image)
+ *
+ * @access public
+ * @param string $filename Source image
+ * @param integer $resize_width Desired image width
+ * @param integer $resize_height Desired image height
+ */
+ public function generateThumbnail($filename, $resize_width, $resize_height)
+ {
+ $metadata = getimagesize($filename);
+ $src_width = $metadata[0];
+ $src_height = $metadata[1];
+ $dst_y = 0;
+ $dst_x = 0;
+
+ if (empty($metadata['mime'])) {
+ return;
+ }
+
+ if ($resize_width == 0 && $resize_height == 0) {
+ $resize_width = 100;
+ $resize_height = 100;
+ }
+
+ if ($resize_width > 0 && $resize_height == 0) {
+ $dst_width = $resize_width;
+ $dst_height = floor($src_height * ($resize_width / $src_width));
+ $dst_image = imagecreatetruecolor($dst_width, $dst_height);
+ }
+ elseif ($resize_width == 0 && $resize_height > 0) {
+ $dst_width = floor($src_width * ($resize_height / $src_height));
+ $dst_height = $resize_height;
+ $dst_image = imagecreatetruecolor($dst_width, $dst_height);
+ }
+ else {
+
+ $src_ratio = $src_width / $src_height;
+ $resize_ratio = $resize_width / $resize_height;
+
+ if ($src_ratio <= $resize_ratio) {
+ $dst_width = $resize_width;
+ $dst_height = floor($src_height * ($resize_width / $src_width));
+
+ $dst_y = ($dst_height - $resize_height) / 2 * (-1);
+ }
+ else {
+ $dst_width = floor($src_width * ($resize_height / $src_height));
+ $dst_height = $resize_height;
+
+ $dst_x = ($dst_width - $resize_width) / 2 * (-1);
+ }
+
+ $dst_image = imagecreatetruecolor($resize_width, $resize_height);
+ }
+
+ switch ($metadata['mime']) {
+ case 'image/jpeg':
+ case 'image/jpg':
+ $src_image = imagecreatefromjpeg($filename);
+ break;
+ case 'image/png':
+ $src_image = imagecreatefrompng($filename);
+ break;
+ case 'image/gif':
+ $src_image = imagecreatefromgif($filename);
+ break;
+ default:
+ return;
+ }
+
+ imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height);
+ imagejpeg($dst_image);
+ }
}
diff --git a/app/Model/HourlyRate.php b/app/Model/HourlyRate.php
new file mode 100644
index 00000000..1550bdae
--- /dev/null
+++ b/app/Model/HourlyRate.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Hourly Rate
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class HourlyRate extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'hourly_rates';
+
+ /**
+ * Get all user rates for a given project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAllByProject($project_id)
+ {
+ $members = $this->projectPermission->getMembers($project_id);
+
+ if (empty($members)) {
+ return array();
+ }
+
+ return $this->db->table(self::TABLE)->in('user_id', array_keys($members))->desc('date_effective')->findAll();
+ }
+
+ /**
+ * Get all rates for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getAllByUser($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_effective')->findAll();
+ }
+
+ /**
+ * Get current rate for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return float
+ */
+ public function getCurrentRate($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_effective')->findOneColumn('rate') ?: 0;
+ }
+
+ /**
+ * Add a new rate in the database
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param float $rate Hourly rate
+ * @param string $currency Currency code
+ * @param string $date ISO8601 date format
+ * @return boolean|integer
+ */
+ public function create($user_id, $rate, $currency, $date)
+ {
+ $values = array(
+ 'user_id' => $user_id,
+ 'rate' => $rate,
+ 'currency' => $currency,
+ 'date_effective' => $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($date)),
+ );
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Remove a specific rate
+ *
+ * @access public
+ * @param integer $rate_id
+ * @return boolean
+ */
+ public function remove($rate_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $rate_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('user_id', t('Field required')),
+ new Validators\Required('rate', t('Field required')),
+ new Validators\Numeric('rate', t('This value must be numeric')),
+ new Validators\Required('date_effective', t('Field required')),
+ new Validators\Required('currency', t('Field required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php
index 3391db50..dd64284e 100644
--- a/app/Model/LastLogin.php
+++ b/app/Model/LastLogin.php
@@ -32,7 +32,7 @@ class LastLogin extends Base
* @param integer $user_id User id
* @param string $ip IP Address
* @param string $user_agent User Agent
- * @return array
+ * @return boolean
*/
public function create($auth_type, $user_id, $ip, $user_agent)
{
diff --git a/app/Model/Link.php b/app/Model/Link.php
new file mode 100644
index 00000000..e0e5184e
--- /dev/null
+++ b/app/Model/Link.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace Model;
+
+use PDO;
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Link model
+ *
+ * @package model
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class Link extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'links';
+
+ /**
+ * Get a link by id
+ *
+ * @access public
+ * @param integer $link_id Link id
+ * @return array
+ */
+ public function getById($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne();
+ }
+
+ /**
+ * Get a link by name
+ *
+ * @access public
+ * @param string $label
+ * @return array
+ */
+ public function getByLabel($label)
+ {
+ return $this->db->table(self::TABLE)->eq('label', $label)->findOne();
+ }
+
+ /**
+ * Get the opposite link id
+ *
+ * @access public
+ * @param integer $link_id Link id
+ * @return integer
+ */
+ public function getOppositeLinkId($link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $link_id)->findOneColumn('opposite_id') ?: $link_id;
+ }
+
+ /**
+ * Get all links
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->db->table(self::TABLE)->findAll();
+ }
+
+ /**
+ * Get merged links
+ *
+ * @access public
+ * @return array
+ */
+ public function getMergedList()
+ {
+ return $this->db
+ ->execute('
+ SELECT
+ links.id, links.label, opposite.label as opposite_label
+ FROM links
+ LEFT JOIN links AS opposite ON opposite.id=links.opposite_id
+ ')
+ ->fetchAll(PDO::FETCH_ASSOC);
+ }
+
+ /**
+ * Get label list
+ *
+ * @access public
+ * @param integer $exclude_id Exclude this link
+ * @param booelan $prepend Prepend default value
+ * @return array
+ */
+ public function getList($exclude_id = 0, $prepend = true)
+ {
+ $labels = $this->db->hashtable(self::TABLE)->neq('id', $exclude_id)->asc('id')->getAll('id', 'label');
+
+ foreach ($labels as &$value) {
+ $value = t($value);
+ }
+
+ return $prepend ? array('') + $labels : $labels;
+ }
+
+ /**
+ * Create a new link label
+ *
+ * @access public
+ * @param string $label
+ * @param string $opposite_label
+ * @return boolean|integer
+ */
+ public function create($label, $opposite_label = '')
+ {
+ $this->db->startTransaction();
+
+ if (! $this->db->table(self::TABLE)->insert(array('label' => $label))) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
+ $label_id = $this->db->getConnection()->getLastId();
+
+ if (! empty($opposite_label)) {
+
+ $this->db
+ ->table(self::TABLE)
+ ->insert(array(
+ 'label' => $opposite_label,
+ 'opposite_id' => $label_id,
+ ));
+
+ $this->db
+ ->table(self::TABLE)
+ ->eq('id', $label_id)
+ ->update(array(
+ 'opposite_id' => $this->db->getConnection()->getLastId()
+ ));
+ }
+
+ $this->db->closeTransaction();
+
+ return (int) $label_id;
+ }
+
+ /**
+ * Update a link
+ *
+ * @access public
+ * @param array $values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $values['id'])
+ ->update(array(
+ 'label' => $values['label'],
+ 'opposite_id' => $values['opposite_id'],
+ ));
+ }
+
+ /**
+ * Remove a link a the relation to its opposite
+ *
+ * @access public
+ * @param integer $link_id
+ * @return boolean
+ */
+ public function remove($link_id)
+ {
+ $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/Notification.php b/app/Model/Notification.php
index 0a80e335..7255a414 100644
--- a/app/Model/Notification.php
+++ b/app/Model/Notification.php
@@ -4,11 +4,6 @@ namespace Model;
use Core\Session;
use Core\Translator;
-use Core\Template;
-use Event\NotificationListener;
-use Swift_Message;
-use Swift_Mailer;
-use Swift_TransportException;
/**
* Notification model
@@ -26,189 +21,326 @@ class Notification extends Base
const TABLE = 'user_has_notifications';
/**
- * Get a list of people with notifications enabled
+ * User filters
+ *
+ * @var integer
+ */
+ const FILTER_NONE = 1;
+ const FILTER_ASSIGNEE = 2;
+ const FILTER_CREATOR = 3;
+ const FILTER_BOTH = 4;
+
+ /**
+ * Send overdue tasks
*
* @access public
- * @param integer $project_id Project id
- * @param array $exlude_users List of user_id to exclude
- * @return array
*/
- public function getUsersWithNotification($project_id, array $exclude_users = array())
+ public function sendOverdueTaskNotifications()
{
- return $this->db
- ->table(ProjectPermission::TABLE)
- ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email')
- ->join(User::TABLE, 'id', 'user_id')
- ->eq('project_id', $project_id)
- ->eq('notifications_enabled', '1')
- ->neq('email', '')
- ->notin(User::TABLE.'.id', $exclude_users)
- ->findAll();
+ $tasks = $this->taskFinder->getOverdueTasks();
+ $projects = array();
+
+ foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) {
+
+ // Get the list of users that should receive notifications for each projects
+ $users = $this->notification->getUsersWithNotificationEnabled($project_id);
+
+ foreach ($users as $user) {
+ $this->sendUserOverdueTaskNotifications($user, $project_tasks);
+ }
+ }
+
+ return $tasks;
}
/**
- * Get the list of users to send the notification for a given project
+ * Send overdue tasks for a given user
*
* @access public
- * @param integer $project_id Project id
- * @param array $exlude_users List of user_id to exclude
- * @return array
+ * @param array $user
+ * @param array $tasks
*/
- public function getUsersList($project_id, array $exclude_users = array())
+ public function sendUserOverdueTaskNotifications(array $user, array $tasks)
{
- // Exclude the connected user
- if (Session::isOpen()) {
- $exclude_users[] = $this->acl->getUserId();
+ $user_tasks = array();
+
+ foreach ($tasks as $task) {
+ if ($this->notification->shouldReceiveNotification($user, array('task' => $task))) {
+ $user_tasks[] = $task;
+ }
}
- $users = $this->getUsersWithNotification($project_id, $exclude_users);
+ if (! empty($user_tasks)) {
+ $this->sendEmailNotification(
+ $user,
+ Task::EVENT_OVERDUE,
+ array('tasks' => $user_tasks, 'project_name' => $tasks[0]['project_name'])
+ );
+ }
+ }
- foreach ($users as $index => $user) {
+ /**
+ * Send notifications to people
+ *
+ * @access public
+ * @param string $event_name
+ * @param array $event_data
+ */
+ public function sendNotifications($event_name, array $event_data)
+ {
+ $logged_user_id = $this->userSession->isLogged() ? $this->userSession->getId() : 0;
+ $users = $this->notification->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id);
- $projects = $this->db->table(self::TABLE)
- ->eq('user_id', $user['id'])
- ->findAllByColumn('project_id');
+ foreach ($users as $user) {
+ if ($this->shouldReceiveNotification($user, $event_data)) {
+ $this->sendEmailNotification($user, $event_name, $event_data);
+ }
+ }
- // The user have selected only some projects
- if (! empty($projects)) {
+ // Restore locales
+ $this->config->setupTranslations();
+ }
- // If the user didn't select this project we remove that guy from the list
- if (! in_array($project_id, $projects)) {
- unset($users[$index]);
- }
- }
+ /**
+ * Send email notification to someone
+ *
+ * @access public
+ * @param array $user User
+ * @param string $event_name
+ * @param array $event_data
+ */
+ public function sendEmailNotification(array $user, $event_name, array $event_data)
+ {
+ // Use the user language otherwise use the application language (do not use the session language)
+ if (! empty($user['language'])) {
+ Translator::load($user['language']);
+ }
+ else {
+ Translator::load($this->config->get('application_language', 'en_US'));
}
- return $users;
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getMailSubject($event_name, $event_data),
+ $this->getMailContent($event_name, $event_data)
+ );
}
/**
- * Attach events
+ * Return true if the user should receive notification
*
* @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
*/
- public function attachEvents()
+ public function shouldReceiveNotification(array $user, array $event_data)
{
- $events = array(
- Task::EVENT_CREATE => 'notification_task_creation',
- Task::EVENT_UPDATE => 'notification_task_update',
- Task::EVENT_CLOSE => 'notification_task_close',
- Task::EVENT_OPEN => 'notification_task_open',
- Task::EVENT_MOVE_COLUMN => 'notification_task_move_column',
- Task::EVENT_MOVE_POSITION => 'notification_task_move_position',
- Task::EVENT_ASSIGNEE_CHANGE => 'notification_task_assignee_change',
- SubTask::EVENT_CREATE => 'notification_subtask_creation',
- SubTask::EVENT_UPDATE => 'notification_subtask_update',
- Comment::EVENT_CREATE => 'notification_comment_creation',
- Comment::EVENT_UPDATE => 'notification_comment_update',
- File::EVENT_CREATE => 'notification_file_creation',
+ $filters = array(
+ 'filterNone',
+ 'filterAssignee',
+ 'filterCreator',
+ 'filterBoth',
);
- foreach ($events as $event_name => $template_name) {
+ foreach ($filters as $filter) {
+ if ($this->$filter($user, $event_data)) {
+ return $this->filterProject($user, $event_data);
+ }
+ }
- $listener = new NotificationListener($this->registry);
- $listener->setTemplate($template_name);
+ return false;
+ }
- $this->event->attach($event_name, $listener);
- }
+ /**
+ * Return true if the user will receive all notifications
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterNone(array $user, array $event_data)
+ {
+ return $user['notifications_filter'] == self::FILTER_NONE;
}
/**
- * Send the email notifications
+ * Return true if the user is the assignee and selected the filter "assignee"
*
* @access public
- * @param string $template Template name
- * @param array $users List of users
- * @param array $data Template data
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
*/
- public function sendEmails($template, array $users, array $data)
+ public function filterAssignee(array $user, array $event_data)
{
- try {
- $transport = $this->registry->shared('mailer');
- $mailer = Swift_Mailer::newInstance($transport);
+ return $user['notifications_filter'] == self::FILTER_ASSIGNEE && $event_data['task']['owner_id'] == $user['id'];
+ }
- $message = Swift_Message::newInstance()
- ->setSubject($this->getMailSubject($template, $data))
- ->setFrom(array(MAIL_FROM => 'Kanboard'))
- ->setBody($this->getMailContent($template, $data), 'text/html');
+ /**
+ * Return true if the user is the creator and enabled the filter "creator"
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterCreator(array $user, array $event_data)
+ {
+ return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id'];
+ }
- foreach ($users as $user) {
- $message->setTo(array($user['email'] => $user['name'] ?: $user['username']));
- $mailer->send($message);
- }
+ /**
+ * Return true if the user is the assignee or the creator and selected the filter "both"
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterBoth(array $user, array $event_data)
+ {
+ return $user['notifications_filter'] == self::FILTER_BOTH &&
+ ($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id']);
+ }
+
+ /**
+ * Return true if the user want to receive notification for the selected project
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterProject(array $user, array $event_data)
+ {
+ $projects = $this->db->table(self::TABLE)->eq('user_id', $user['id'])->findAllByColumn('project_id');
+
+ if (! empty($projects)) {
+ return in_array($event_data['task']['project_id'], $projects);
}
- catch (Swift_TransportException $e) {
- debug($e->getMessage());
+
+ return true;
+ }
+
+ /**
+ * Get a list of people with notifications enabled
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param array $exclude_user_id User id to exclude
+ * @return array
+ */
+ public function getUsersWithNotificationEnabled($project_id, $exclude_user_id = 0)
+ {
+ if ($this->projectPermission->isEverybodyAllowed($project_id)) {
+
+ return $this->db
+ ->table(User::TABLE)
+ ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter')
+ ->eq('notifications_enabled', '1')
+ ->neq('email', '')
+ ->neq(User::TABLE.'.id', $exclude_user_id)
+ ->findAll();
}
+
+ return $this->db
+ ->table(ProjectPermission::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)
+ ->eq('notifications_enabled', '1')
+ ->neq('email', '')
+ ->neq(User::TABLE.'.id', $exclude_user_id)
+ ->findAll();
+ }
+
+ /**
+ * Get the mail content for a given template name
+ *
+ * @access public
+ * @param string $event_name Event name
+ * @param array $event_data Event data
+ * @return string
+ */
+ public function getMailContent($event_name, array $event_data)
+ {
+ return $this->template->render(
+ 'notification/'.str_replace('.', '_', $event_name),
+ $event_data + array('application_url' => $this->config->get('application_url'))
+ );
}
/**
* Get the mail subject for a given template name
*
* @access public
- * @param string $template Template name
- * @param array $data Template data
+ * @param string $event_name Event name
+ * @param array $event_data Event data
+ * @return string
*/
public function getMailSubject($template, array $data)
{
switch ($template) {
- case 'notification_file_creation':
- $subject = e('[%s][New attachment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Task::EVENT_CREATE:
+ $subject = $this->getStandardMailSubject(e('New attachment'), $data);
break;
- case 'notification_comment_creation':
- $subject = e('[%s][New comment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Comment::EVENT_CREATE:
+ $subject = $this->getStandardMailSubject(e('New comment'), $data);
break;
- case 'notification_comment_update':
- $subject = e('[%s][Comment updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Comment::EVENT_UPDATE:
+ $subject = $this->getStandardMailSubject(e('Comment updated'), $data);
break;
- case 'notification_subtask_creation':
- $subject = e('[%s][New subtask] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Subtask::EVENT_CREATE:
+ $subject = $this->getStandardMailSubject(e('New subtask'), $data);
break;
- case 'notification_subtask_update':
- $subject = e('[%s][Subtask updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Subtask::EVENT_UPDATE:
+ $subject = $this->getStandardMailSubject(e('Subtask updated'), $data);
break;
- case 'notification_task_creation':
- $subject = e('[%s][New task] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Task::EVENT_CREATE:
+ $subject = $this->getStandardMailSubject(e('New task'), $data);
break;
- case 'notification_task_update':
- $subject = e('[%s][Task updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Task::EVENT_UPDATE:
+ $subject = $this->getStandardMailSubject(e('Task updated'), $data);
break;
- case 'notification_task_close':
- $subject = e('[%s][Task closed] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Task::EVENT_CLOSE:
+ $subject = $this->getStandardMailSubject(e('Task closed'), $data);
break;
- case 'notification_task_open':
- $subject = e('[%s][Task opened] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Task::EVENT_OPEN:
+ $subject = $this->getStandardMailSubject(e('Task opened'), $data);
break;
- case 'notification_task_move_column':
- $subject = e('[%s][Column Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Task::EVENT_MOVE_COLUMN:
+ $subject = $this->getStandardMailSubject(e('Column Change'), $data);
break;
- case 'notification_task_move_position':
- $subject = e('[%s][Position Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Task::EVENT_MOVE_POSITION:
+ $subject = $this->getStandardMailSubject(e('Position Change'), $data);
break;
- case 'notification_task_assignee_change':
- $subject = e('[%s][Assignee Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']);
+ case Task::EVENT_ASSIGNEE_CHANGE:
+ $subject = $this->getStandardMailSubject(e('Assignee Change'), $data);
break;
- case 'notification_task_due':
- $subject = e('[%s][Due tasks]', $data['project']);
+ case Task::EVENT_OVERDUE:
+ $subject = e('[%s] Overdue tasks', $data['project_name']);
break;
default:
- $subject = e('[Kanboard] Notification');
+ $subject = e('Notification');
}
return $subject;
}
/**
- * Get the mail content for a given template name
+ * Get the mail subject for a given label
*
- * @access public
- * @param string $template Template name
+ * @access private
+ * @param string $label Label
* @param array $data Template data
+ * @return string
*/
- public function getMailContent($template, array $data)
+ private function getStandardMailSubject($label, array $data)
{
- $tpl = new Template;
- return $tpl->load($template, $data + array('application_url' => $this->config->get('application_url')));
+ return sprintf('[%s][%s] %s (#%d)', $data['task']['project_name'], $label, $data['task']['title'], $data['task']['id']);
}
/**
@@ -227,7 +359,8 @@ class Notification extends Base
// Activate notifications
$this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
- 'notifications_enabled' => '1'
+ 'notifications_enabled' => '1',
+ 'notifications_filter' => empty($values['notifications_filter']) ? self::FILTER_BOTH : $values['notifications_filter'],
));
// Save selected projects
@@ -259,9 +392,7 @@ class Notification extends Base
*/
public function readSettings($user_id)
{
- $values = array();
- $values['notifications_enabled'] = $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_enabled');
-
+ $values = $this->db->table(User::TABLE)->eq('id', $user_id)->columns('notifications_enabled', 'notifications_filter')->findOne();
$projects = $this->db->table(self::TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id');
foreach ($projects as $project_id) {
diff --git a/app/Model/Project.php b/app/Model/Project.php
index 32b7fcbe..71c660b9 100644
--- a/app/Model/Project.php
+++ b/app/Model/Project.php
@@ -4,7 +4,6 @@ namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
-use Event\ProjectModificationDateListener;
use Core\Security;
/**
@@ -52,12 +51,28 @@ class Project extends Base
* Get a project by the name
*
* @access public
- * @param string $project_name Project name
+ * @param string $name Project name
* @return array
*/
- public function getByName($project_name)
+ public function getByName($name)
{
- return $this->db->table(self::TABLE)->eq('name', $project_name)->findOne();
+ return $this->db->table(self::TABLE)->eq('name', $name)->findOne();
+ }
+
+ /**
+ * Get a project by the identifier (code)
+ *
+ * @access public
+ * @param string $identifier
+ * @return array|boolean
+ */
+ public function getByIdentifier($identifier)
+ {
+ if (empty($identifier)) {
+ return false;
+ }
+
+ return $this->db->table(self::TABLE)->eq('identifier', strtoupper($identifier))->findOne();
}
/**
@@ -65,10 +80,14 @@ class Project extends Base
*
* @access public
* @param string $token Token
- * @return array
+ * @return array|boolean
*/
public function getByToken($token)
{
+ if (empty($token)) {
+ return false;
+ }
+
return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_public', 1)->findOne();
}
@@ -96,27 +115,25 @@ class Project extends Base
}
/**
- * Get all projects, optionaly fetch stats for each project and can check users permissions
+ * Get all projects
*
* @access public
- * @param bool $filter_permissions If true, remove projects not allowed for the current user
* @return array
*/
- public function getAll($filter_permissions = false)
+ public function getAll()
{
- $projects = $this->db->table(self::TABLE)->asc('name')->findAll();
-
- if ($filter_permissions) {
-
- foreach ($projects as $key => $project) {
-
- if (! $this->projectPermission->isUserAllowed($project['id'], $this->acl->getUserId())) {
- unset($projects[$key]);
- }
- }
- }
+ return $this->db->table(self::TABLE)->asc('name')->findAll();
+ }
- return $projects;
+ /**
+ * Get all project ids
+ *
+ * @access public
+ * @return array
+ */
+ public function getAllIds()
+ {
+ return $this->db->table(self::TABLE)->asc('name')->findAllByColumn('id');
}
/**
@@ -129,10 +146,10 @@ class Project extends Base
public function getList($prepend = true)
{
if ($prepend) {
- return array(t('None')) + $this->db->table(self::TABLE)->asc('name')->listing('id', 'name');
+ return array(t('None')) + $this->db->hashtable(self::TABLE)->asc('name')->getAll('id', 'name');
}
- return $this->db->table(self::TABLE)->asc('name')->listing('id', 'name');
+ return $this->db->hashtable(self::TABLE)->asc('name')->getAll('id', 'name');
}
/**
@@ -161,10 +178,10 @@ class Project extends Base
public function getListByStatus($status)
{
return $this->db
- ->table(self::TABLE)
+ ->hashtable(self::TABLE)
->asc('name')
->eq('is_active', $status)
- ->listing('id', 'name');
+ ->getAll('id', 'name');
}
/**
@@ -189,14 +206,15 @@ class Project extends Base
* @param integer $project_id Project id
* @return array
*/
- public function getStats($project_id)
+ public function getTaskStats($project_id)
{
$stats = array();
- $columns = $this->board->getcolumns($project_id);
$stats['nb_active_tasks'] = 0;
+ $columns = $this->board->getColumns($project_id);
+ $column_stats = $this->board->getColumnStats($project_id);
foreach ($columns as &$column) {
- $column['nb_active_tasks'] = $this->taskFinder->countByColumnId($project_id, $column['id']);
+ $column['nb_active_tasks'] = isset($column_stats[$column['id']]) ? $column_stats[$column['id']] : 0;
$stats['nb_active_tasks'] += $column['nb_active_tasks'];
}
@@ -208,73 +226,69 @@ class Project extends Base
}
/**
- * Create a project from another one.
+ * Get stats for each column of a project
*
- * @author Antonio Rabelo
- * @param integer $project_id Project Id
- * @return integer Cloned Project Id
+ * @access public
+ * @param array $project
+ * @return array
*/
- public function createProjectFromAnotherProject($project_id)
+ public function getColumnStats(array &$project)
{
- $project = $this->getById($project_id);
-
- $values = array(
- 'name' => $project['name'].' ('.t('Clone').')',
- 'is_active' => true,
- 'last_modified' => 0,
- 'token' => '',
- 'is_public' => 0,
- 'is_private' => empty($project['is_private']) ? 0 : 1,
- );
+ $project['columns'] = $this->board->getColumns($project['id']);
+ $stats = $this->board->getColumnStats($project['id']);
- if (! $this->db->table(self::TABLE)->save($values)) {
- return false;
+ foreach ($project['columns'] as &$column) {
+ $column['nb_tasks'] = isset($stats[$column['id']]) ? $stats[$column['id']] : 0;
}
- return $this->db->getConnection()->getLastId();
+ return $project;
}
/**
- * Clone a project
+ * Apply column stats to a collection of projects (filter callback)
*
- * @author Antonio Rabelo
- * @param integer $project_id Project Id
- * @return integer Cloned Project Id
+ * @access public
+ * @param array $projects
+ * @return array
*/
- public function duplicate($project_id)
+ public function applyColumnStats(array $projects)
{
- $this->db->startTransaction();
-
- // Get the cloned project Id
- $clone_project_id = $this->createProjectFromAnotherProject($project_id);
-
- if (! $clone_project_id) {
- $this->db->cancelTransaction();
- return false;
+ foreach ($projects as &$project) {
+ $this->getColumnStats($project);
}
- foreach (array('board', 'category', 'projectPermission', 'action') as $model) {
+ return $projects;
+ }
- if (! $this->$model->duplicate($project_id, $clone_project_id)) {
- $this->db->cancelTransaction();
- return false;
- }
+ /**
+ * Get project summary for a list of project
+ *
+ * @access public
+ * @param array $project_ids List of project id
+ * @return \PicoDb\Table
+ */
+ public function getQueryColumnStats(array $project_ids)
+ {
+ if (empty($project_ids)) {
+ return $this->db->table(Project::TABLE)->limit(0);
}
- $this->db->closeTransaction();
-
- return (int) $clone_project_id;
+ return $this->db
+ ->table(Project::TABLE)
+ ->in('id', $project_ids)
+ ->filter(array($this, 'applyColumnStats'));
}
/**
* Create a project
*
* @access public
- * @param array $values Form values
- * @param integer $user_id User who create the project
- * @return integer Project id
+ * @param array $values Form values
+ * @param integer $user_id User who create the project
+ * @param bool $add_user Automatically add the user
+ * @return integer Project id
*/
- public function create(array $values, $user_id = 0)
+ public function create(array $values, $user_id = 0, $add_user = false)
{
$this->db->startTransaction();
@@ -282,6 +296,10 @@ class Project extends Base
$values['last_modified'] = time();
$values['is_private'] = empty($values['is_private']) ? 0 : 1;
+ if (! empty($values['identifier'])) {
+ $values['identifier'] = strtoupper($values['identifier']);
+ }
+
if (! $this->db->table(self::TABLE)->save($values)) {
$this->db->cancelTransaction();
return false;
@@ -294,10 +312,12 @@ class Project extends Base
return false;
}
- if ($values['is_private'] && $user_id) {
- $this->projectPermission->allowUser($project_id, $user_id);
+ if ($add_user && $user_id) {
+ $this->projectPermission->addManager($project_id, $user_id);
}
+ $this->category->createDefaultCategories($project_id);
+
$this->db->closeTransaction();
return (int) $project_id;
@@ -342,6 +362,10 @@ class Project extends Base
*/
public function update(array $values)
{
+ if (! empty($values['identifier'])) {
+ $values['identifier'] = strtoupper($values['identifier']);
+ }
+
return $this->exists($values['id']) &&
$this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
}
@@ -447,7 +471,10 @@ class Project extends Base
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\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),
);
}
@@ -460,6 +487,10 @@ class Project extends Base
*/
public function validateCreation(array $values)
{
+ if (! empty($values['identifier'])) {
+ $values['identifier'] = strtoupper($values['identifier']);
+ }
+
$v = new Validator($values, $this->commonValidationRules());
return array(
@@ -477,6 +508,10 @@ class Project extends Base
*/
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')),
);
@@ -488,34 +523,4 @@ class Project extends Base
$v->getErrors()
);
}
-
- /**
- * Attach events
- *
- * @access public
- */
- public function attachEvents()
- {
- $events = array(
- Task::EVENT_CREATE_UPDATE,
- Task::EVENT_CLOSE,
- Task::EVENT_OPEN,
- Task::EVENT_MOVE_COLUMN,
- Task::EVENT_MOVE_POSITION,
- Task::EVENT_ASSIGNEE_CHANGE,
- GithubWebhook::EVENT_ISSUE_OPENED,
- GithubWebhook::EVENT_ISSUE_CLOSED,
- GithubWebhook::EVENT_ISSUE_REOPENED,
- GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE,
- GithubWebhook::EVENT_ISSUE_LABEL_CHANGE,
- GithubWebhook::EVENT_ISSUE_COMMENT,
- GithubWebhook::EVENT_COMMIT,
- );
-
- $listener = new ProjectModificationDateListener($this->registry);
-
- foreach ($events as $event_name) {
- $this->event->attach($event_name, $listener);
- }
- }
}
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index 6d6ef454..27f1cfcd 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -2,9 +2,6 @@
namespace Model;
-use Core\Template;
-use Event\ProjectActivityListener;
-
/**
* Project activity model
*
@@ -25,7 +22,7 @@ class ProjectActivity extends Base
*
* @var integer
*/
- const MAX_EVENTS = 5000;
+ const MAX_EVENTS = 1000;
/**
* Add a new event for the project
@@ -46,7 +43,7 @@ class ProjectActivity extends Base
'creator_id' => $creator_id,
'event_name' => $event_name,
'date_creation' => time(),
- 'data' => serialize($data),
+ 'data' => json_encode($data),
);
$this->cleanup(self::MAX_EVENTS - 1);
@@ -59,42 +56,101 @@ class ProjectActivity extends Base
* @access public
* @param integer $project_id Project id
* @param integer $limit Maximum events number
+ * @param integer $start Timestamp of earliest activity
+ * @param integer $end Timestamp of latest activity
* @return array
*/
- public function getProject($project_id, $limit = 50)
+ public function getProject($project_id, $limit = 50, $start = null, $end = null)
{
- return $this->getProjects(array($project_id), $limit);
+ return $this->getProjects(array($project_id), $limit, $start, $end);
}
/**
* Get all events for the given projects list
*
* @access public
- * @param integer $project_id Project id
+ * @param integer[] $project_ids Projects id
* @param integer $limit Maximum events number
+ * @param integer $start Timestamp of earliest activity
+ * @param integer $end Timestamp of latest activity
* @return array
*/
- public function getProjects(array $projects, $limit = 50)
+ public function getProjects(array $project_ids, $limit = 50, $start = null, $end = null)
{
- if (empty($projects)) {
+ if (empty($project_ids)) {
return array();
}
- $events = $this->db->table(self::TABLE)
- ->columns(
- self::TABLE.'.*',
- User::TABLE.'.username AS author_username',
- User::TABLE.'.name AS author_name'
- )
- ->in('project_id', $projects)
- ->join(User::TABLE, 'id', 'creator_id')
- ->desc('id')
- ->limit($limit)
- ->findAll();
+ $query = $this
+ ->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.*',
+ User::TABLE.'.username AS author_username',
+ User::TABLE.'.name AS author_name',
+ User::TABLE.'.email'
+ )
+ ->in('project_id', $project_ids)
+ ->join(User::TABLE, 'id', 'creator_id')
+ ->desc(self::TABLE.'.id')
+ ->limit($limit);
+
+ return $this->getEvents($query, $start, $end);
+ }
+
+ /**
+ * Get all events for the given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param integer $limit Maximum events number
+ * @param integer $start Timestamp of earliest activity
+ * @param integer $end Timestamp of latest activity
+ * @return array
+ */
+ public function getTask($task_id, $limit = 50, $start = null, $end = null)
+ {
+ $query = $this
+ ->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.*',
+ User::TABLE.'.username AS author_username',
+ User::TABLE.'.name AS author_name',
+ User::TABLE.'.email'
+ )
+ ->eq('task_id', $task_id)
+ ->join(User::TABLE, 'id', 'creator_id')
+ ->desc(self::TABLE.'.id')
+ ->limit($limit);
+
+ return $this->getEvents($query, $start, $end);
+ }
+
+ /**
+ * Common function to return events
+ *
+ * @access public
+ * @param \PicoDb\Table $query PicoDb Query
+ * @param integer $start Timestamp of earliest activity
+ * @param integer $end Timestamp of latest activity
+ * @return array
+ */
+ private function getEvents(\PicoDb\Table $query, $start, $end)
+ {
+ if (! is_null($start)){
+ $query->gte('date_creation', $start);
+ }
+
+ if (! is_null($end)){
+ $query->lte('date_creation', $end);
+ }
+
+ $events = $query->findAll();
foreach ($events as &$event) {
- $event += unserialize($event['data']);
+ $event += $this->decode($event['data']);
unset($event['data']);
$event['author'] = $event['author_name'] ?: $event['author_username'];
@@ -127,34 +183,6 @@ class ProjectActivity extends Base
}
/**
- * Attach events to be able to record the history
- *
- * @access public
- */
- public function attachEvents()
- {
- $events = array(
- Task::EVENT_ASSIGNEE_CHANGE,
- Task::EVENT_UPDATE,
- Task::EVENT_CREATE,
- Task::EVENT_CLOSE,
- Task::EVENT_OPEN,
- Task::EVENT_MOVE_COLUMN,
- Task::EVENT_MOVE_POSITION,
- Comment::EVENT_UPDATE,
- Comment::EVENT_CREATE,
- SubTask::EVENT_UPDATE,
- SubTask::EVENT_CREATE,
- );
-
- $listener = new ProjectActivityListener($this->registry);
-
- foreach ($events as $event_name) {
- $this->event->attach($event_name, $listener);
- }
- }
-
- /**
* Get the event html content
*
* @access public
@@ -163,8 +191,10 @@ class ProjectActivity extends Base
*/
public function getContent(array $params)
{
- $tpl = new Template;
- return $tpl->load('event_'.str_replace('.', '_', $params['event_name']), $params);
+ return $this->template->render(
+ 'event/'.str_replace('.', '_', $params['event_name']),
+ $params
+ );
}
/**
@@ -178,7 +208,13 @@ class ProjectActivity extends Base
{
switch ($event['event_name']) {
case Task::EVENT_ASSIGNEE_CHANGE:
- return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $event['task']['assignee_name'] ?: $event['task']['assignee_username']);
+ $assignee = $event['task']['assignee_name'] ?: $event['task']['assignee_username'];
+
+ if (! empty($assignee)) {
+ return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $assignee);
+ }
+
+ return t('%s remove the assignee of the task %s', $event['author'], e('#%d', $event['task']['id']));
case Task::EVENT_UPDATE:
return t('%s updated the task #%d', $event['author'], $event['task']['id']);
case Task::EVENT_CREATE:
@@ -191,9 +227,9 @@ class ProjectActivity extends Base
return t('%s moved the task #%d to the column "%s"', $event['author'], $event['task']['id'], $event['task']['column_title']);
case Task::EVENT_MOVE_POSITION:
return t('%s moved the task #%d to the position %d in the column "%s"', $event['author'], $event['task']['id'], $event['task']['position'], $event['task']['column_title']);
- case SubTask::EVENT_UPDATE:
+ case Subtask::EVENT_UPDATE:
return t('%s updated a subtask for the task #%d', $event['author'], $event['task']['id']);
- case SubTask::EVENT_CREATE:
+ case Subtask::EVENT_CREATE:
return t('%s created a subtask for the task #%d', $event['author'], $event['task']['id']);
case Comment::EVENT_UPDATE:
return t('%s updated a comment on the task #%d', $event['author'], $event['task']['id']);
@@ -203,4 +239,20 @@ class ProjectActivity extends Base
return '';
}
}
+
+ /**
+ * Decode event data, supports unserialize() and json_decode()
+ *
+ * @access public
+ * @param string $data Serialized data
+ * @return array
+ */
+ public function decode($data)
+ {
+ if ($data{0} === 'a') {
+ return unserialize($data);
+ }
+
+ return json_decode($data, true) ?: array();
+ }
}
diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php
new file mode 100644
index 00000000..a663f921
--- /dev/null
+++ b/app/Model/ProjectAnalytic.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace 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 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);
+ }
+}
diff --git a/app/Model/ProjectDailySummary.php b/app/Model/ProjectDailySummary.php
new file mode 100644
index 00000000..65a612f6
--- /dev/null
+++ b/app/Model/ProjectDailySummary.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Model;
+
+/**
+ * Project daily summary
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectDailySummary extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_daily_summaries';
+
+ /**
+ * Update daily totals for the project
+ *
+ * "total" is the number open of tasks in the column
+ * "score" is the sum of tasks score in the column
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $date Record date (YYYY-MM-DD)
+ * @return boolean
+ */
+ public function updateTotals($project_id, $date)
+ {
+ return $this->db->transaction(function($db) use ($project_id, $date) {
+
+ $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(ProjectDailySummary::TABLE)->insert(array(
+ 'day' => $date,
+ 'project_id' => $project_id,
+ 'column_id' => $column_id,
+ 'total' => 0,
+ 'score' => 0,
+ ));
+
+ $db->table(ProjectDailySummary::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)
+ ->count()
+ ));
+ }
+ });
+ }
+
+ /**
+ * Count the number of recorded days for the data range
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $from Start date (ISO format YYYY-MM-DD)
+ * @param string $to End date
+ * @return integer
+ */
+ public function countDays($project_id, $from, $to)
+ {
+ $rq = $this->db->execute(
+ 'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?',
+ array($from, $to, $project_id)
+ );
+
+ return $rq !== false ? $rq->fetchColumn(0) : 0;
+ }
+
+ /**
+ * Get raw metrics for the project within a data range
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $from Start date (ISO format YYYY-MM-DD)
+ * @param string $to End date
+ * @return array
+ */
+ public function getRawMetrics($project_id, $from, $to)
+ {
+ return $this->db->table(ProjectDailySummary::TABLE)
+ ->columns(
+ ProjectDailySummary::TABLE.'.column_id',
+ ProjectDailySummary::TABLE.'.day',
+ ProjectDailySummary::TABLE.'.total',
+ ProjectDailySummary::TABLE.'.score',
+ Board::TABLE.'.title AS column_title'
+ )
+ ->join(Board::TABLE, 'id', 'column_id')
+ ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->asc(ProjectDailySummary::TABLE.'.day')
+ ->findAll();
+ }
+
+ /**
+ * Get raw metrics for the project within a data range grouped by day
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $from Start date (ISO format YYYY-MM-DD)
+ * @param string $to End date
+ * @return array
+ */
+ public function getRawMetricsByDay($project_id, $from, $to)
+ {
+ return $this->db->table(ProjectDailySummary::TABLE)
+ ->columns(
+ ProjectDailySummary::TABLE.'.day',
+ 'SUM('.ProjectDailySummary::TABLE.'.total) AS total',
+ 'SUM('.ProjectDailySummary::TABLE.'.score) AS score'
+ )
+ ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->asc(ProjectDailySummary::TABLE.'.day')
+ ->groupBy(ProjectDailySummary::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],
+ * ]
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $from Start date (ISO format YYYY-MM-DD)
+ * @param string $to End date
+ * @return array
+ */
+ public function getAggregatedMetrics($project_id, $from, $to)
+ {
+ $columns = $this->board->getColumnsList($project_id);
+ $column_ids = array_keys($columns);
+ $metrics = array(array(e('Date')) + $columns);
+ $aggregates = array();
+
+ // Fetch metrics for the project
+ $records = $this->db->table(ProjectDailySummary::TABLE)
+ ->eq('project_id', $project_id)
+ ->gte('day', $from)
+ ->lte('day', $to)
+ ->findAll();
+
+ // Aggregate by day
+ foreach ($records as $record) {
+
+ if (! isset($aggregates[$record['day']])) {
+ $aggregates[$record['day']] = array($record['day']);
+ }
+
+ $aggregates[$record['day']][$record['column_id']] = $record['total'];
+ }
+
+ // Aggregate by row
+ foreach ($aggregates as $aggregate) {
+
+ $row = array($aggregate[0]);
+
+ foreach ($column_ids as $column_id) {
+ $row[] = (int) $aggregate[$column_id];
+ }
+
+ $metrics[] = $row;
+ }
+
+ return $metrics;
+ }
+}
diff --git a/app/Model/ProjectDuplication.php b/app/Model/ProjectDuplication.php
new file mode 100644
index 00000000..7e3407be
--- /dev/null
+++ b/app/Model/ProjectDuplication.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Model;
+
+/**
+ * Project Duplication
+ *
+ * @package model
+ * @author Frederic Guillot
+ * @author Antonio Rabelo
+ */
+class ProjectDuplication extends Base
+{
+ /**
+ * Get a valid project name for the duplication
+ *
+ * @access public
+ * @param string $name Project name
+ * @param integer $max_length Max length allowed
+ * @return string
+ */
+ public function getClonedProjectName($name, $max_length = 50)
+ {
+ $suffix = ' ('.t('Clone').')';
+
+ if (strlen($name.$suffix) > $max_length) {
+ $name = substr($name, 0, $max_length - strlen($suffix));
+ }
+
+ return $name.$suffix;
+ }
+
+ /**
+ * 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->getConnection()->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
+ */
+ public function duplicate($project_id, $part_selection = array('category', 'action'))
+ {
+ $this->db->startTransaction();
+
+ // Get the cloned project Id
+ $clone_project_id = $this->copy($project_id);
+
+ if (! $clone_project_id) {
+ $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) {
+
+ // Skip if optional part has not been selected
+ if (in_array($model, $optional_parts) && ! in_array($model, $part_selection)) {
+ continue;
+ }
+
+ if (! $this->$model->duplicate($project_id, $clone_project_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);
+
+ foreach ($tasks as $task) {
+ if (! $this->taskDuplication->duplicateToProject($task['id'], $clone_project_id)) {
+ return false;
+ }
+ }
+ }
+
+ return (int) $clone_project_id;
+ }
+}
diff --git a/app/Model/ProjectIntegration.php b/app/Model/ProjectIntegration.php
new file mode 100644
index 00000000..98ff8d4c
--- /dev/null
+++ b/app/Model/ProjectIntegration.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Model;
+
+/**
+ * Project integration
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class ProjectIntegration extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'project_integrations';
+
+ /**
+ * Get all parameters for a project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getParameters($project_id)
+ {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->findOne() ?: array();
+ }
+
+ /**
+ * Save parameters for a project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param array $values
+ * @return boolean
+ */
+ public function saveParameters($project_id, array $values)
+ {
+ if ($this->db->table(self::TABLE)->eq('project_id', $project_id)->count() === 1) {
+ return $this->db->table(self::TABLE)->eq('project_id', $project_id)->update($values);
+ }
+
+ return $this->db->table(self::TABLE)->insert($values + array('project_id' => $project_id));
+ }
+
+ /**
+ * Check if a project has the given parameter/value
+ *
+ * @access public
+ * @param integer $project_id
+ * @param string $option
+ * @param string $value
+ * @return boolean
+ */
+ public function hasValue($project_id, $option, $value)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq($option, $value)
+ ->count() === 1;
+ }
+}
diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php
index fb9847b5..b0a09df4 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -27,11 +27,16 @@ class ProjectPermission extends Base
* @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 getUsersList($project_id, $prepend_unassigned = true, $prepend_everybody = false)
+ public function getMemberList($project_id, $prepend_unassigned = true, $prepend_everybody = false, $allow_single_user = false)
{
- $allowed_users = $this->getAllowedUsers($project_id);
+ $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;
@@ -51,7 +56,7 @@ class ProjectPermission extends Base
* @param integer $project_id Project id
* @return array
*/
- public function getAllowedUsers($project_id)
+ public function getMembers($project_id)
{
if ($this->isEverybodyAllowed($project_id)) {
return $this->user->getList();
@@ -81,6 +86,27 @@ class ProjectPermission extends Base
}
/**
+ * 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 allowed and not allowed users for a project
*
* @access public
@@ -92,11 +118,13 @@ class ProjectPermission extends Base
$users = array(
'allowed' => array(),
'not_allowed' => array(),
+ 'managers' => array(),
);
$all_users = $this->user->getList();
- $users['allowed'] = $this->getAllowedUsers($project_id);
+ $users['allowed'] = $this->getMembers($project_id);
+ $users['managers'] = $this->getManagers($project_id);
foreach ($all_users as $user_id => $username) {
@@ -109,14 +137,14 @@ class ProjectPermission extends Base
}
/**
- * Allow a specific user for a given project
+ * Add a new project member
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return bool
*/
- public function allowUser($project_id, $user_id)
+ public function addMember($project_id, $user_id)
{
return $this->db
->table(self::TABLE)
@@ -124,14 +152,14 @@ class ProjectPermission extends Base
}
/**
- * Revoke a specific user for a given project
+ * Remove a member
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return bool
*/
- public function revokeUser($project_id, $user_id)
+ public function revokeMember($project_id, $user_id)
{
return $this->db
->table(self::TABLE)
@@ -141,61 +169,104 @@ class ProjectPermission extends Base
}
/**
- * Check if a specific user is allowed to access to a given project
+ * Add a project manager
*
* @access public
* @param integer $project_id Project id
* @param integer $user_id User id
* @return bool
*/
- public function isUserAllowed($project_id, $user_id)
+ public function addManager($project_id, $user_id)
{
- if ($this->user->isAdmin($user_id)) {
- return true;
- }
+ 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 (bool) $this->db
+ return $this->db
->table(self::TABLE)
->eq('project_id', $project_id)
->eq('user_id', $user_id)
- ->count();
- }
+ ->count() === 1;
+ }
- /**
- * Return true if everybody is allowed for the project
+ /**
+ * 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 isEverybodyAllowed($project_id)
+ public function isManager($project_id, $user_id)
{
- return (bool) $this->db
- ->table(Project::TABLE)
- ->eq('id', $project_id)
- ->eq('is_everybody_allowed', 1)
- ->count();
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('user_id', $user_id)
+ ->eq('is_owner', 1)
+ ->count() === 1;
}
/**
- * Check if a specific user is allowed to manage a project
+ * 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 adminAllowed($project_id, $user_id)
+ public function isUserAllowed($project_id, $user_id)
{
- if ($this->isUserAllowed($project_id, $user_id) && $this->project->isPrivate($project_id)) {
- return true;
- }
+ return $project_id === 0 || $this->user->isAdmin($user_id) || $this->isMember($project_id, $user_id);
+ }
- return false;
+ /**
+ * Return true if everybody is allowed for the project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return bool
+ */
+ public function isEverybodyAllowed($project_id)
+ {
+ return $this->db
+ ->table(Project::TABLE)
+ ->eq('id', $project_id)
+ ->eq('is_everybody_allowed', 1)
+ ->count() === 1;
}
/**
@@ -204,12 +275,13 @@ class ProjectPermission extends Base
* @access public
* @param array $projects Project list: ['project_id' => 'project_name']
* @param integer $user_id User id
+ * @param string $filter Method name to apply
* @return array
*/
- public function filterProjects(array $projects, $user_id)
+ public function filterProjects(array $projects, $user_id, $filter = 'isUserAllowed')
{
foreach ($projects as $project_id => $project_name) {
- if (! $this->isUserAllowed($project_id, $user_id)) {
+ if (! $this->$filter($project_id, $user_id)) {
unset($projects[$project_id]);
}
}
@@ -218,7 +290,7 @@ class ProjectPermission extends Base
}
/**
- * Return a list of projects for a given user
+ * Return a list of allowed active projects for a given user
*
* @access public
* @param integer $user_id User id
@@ -226,23 +298,117 @@ class ProjectPermission extends Base
*/
public function getAllowedProjects($user_id)
{
- return $this->filterProjects($this->project->getListByStatus(Project::ACTIVE), $user_id);
+ if ($this->user->isAdmin($user_id)) {
+ return $this->project->getListByStatus(Project::ACTIVE);
+ }
+
+ 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 a list of project ids where the user is member
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getMemberProjectIds($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 a list of active project ids where the user is member
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getActiveMemberProjectIds($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 a list of active projects where the user is member
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getActiveMemberProjects($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');
}
/**
* Copy user access from a project to another one
*
- * @author Antonio Rabelo
- * @param integer $project_from Project Template
- * @return integer $project_to Project that receives the copy
+ * @param integer $project_src Project Template
+ * @return integer $project_dst Project that receives the copy
* @return boolean
*/
- public function duplicate($project_from, $project_to)
+ public function duplicate($project_src, $project_dst)
{
- $users = $this->getAllowedUsers($project_from);
-
- foreach ($users as $user_id => $name) {
- if (! $this->allowUser($project_to, $user_id)) {
+ $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;
}
}
@@ -264,6 +430,7 @@ class ProjectPermission extends Base
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(
diff --git a/app/Model/SubTask.php b/app/Model/SubTask.php
deleted file mode 100644
index 886ad1f3..00000000
--- a/app/Model/SubTask.php
+++ /dev/null
@@ -1,277 +0,0 @@
-<?php
-
-namespace Model;
-
-use SimpleValidator\Validator;
-use SimpleValidator\Validators;
-
-/**
- * Subtask model
- *
- * @package model
- * @author Frederic Guillot
- */
-class SubTask extends Base
-{
- /**
- * SQL table name
- *
- * @var string
- */
- const TABLE = 'task_has_subtasks';
-
- /**
- * Task "done" status
- *
- * @var integer
- */
- const STATUS_DONE = 2;
-
- /**
- * Task "in progress" status
- *
- * @var integer
- */
- const STATUS_INPROGRESS = 1;
-
- /**
- * Task "todo" status
- *
- * @var integer
- */
- const STATUS_TODO = 0;
-
- /**
- * Events
- *
- * @var string
- */
- const EVENT_UPDATE = 'subtask.update';
- const EVENT_CREATE = 'subtask.create';
-
- /**
- * Get available status
- *
- * @access public
- * @return array
- */
- public function getStatusList()
- {
- $status = array(
- self::STATUS_TODO => t('Todo'),
- self::STATUS_INPROGRESS => t('In progress'),
- self::STATUS_DONE => t('Done'),
- );
-
- asort($status);
-
- return $status;
- }
-
- /**
- * Get all subtasks for a given task
- *
- * @access public
- * @param integer $task_id Task id
- * @return array
- */
- public function getAll($task_id)
- {
- $status = $this->getStatusList();
- $subtasks = $this->db->table(self::TABLE)
- ->eq('task_id', $task_id)
- ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name')
- ->join(User::TABLE, 'id', 'user_id')
- ->asc(self::TABLE.'.id')
- ->findAll();
-
- foreach ($subtasks as &$subtask) {
- $subtask['status_name'] = $status[$subtask['status']];
- }
-
- return $subtasks;
- }
-
- /**
- * Get a subtask by the id
- *
- * @access public
- * @param integer $subtask_id Subtask id
- * @param bool $more Fetch more data
- * @return array
- */
- public function getById($subtask_id, $more = false)
- {
- if ($more) {
-
- $subtask = $this->db->table(self::TABLE)
- ->eq(self::TABLE.'.id', $subtask_id)
- ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name')
- ->join(User::TABLE, 'id', 'user_id')
- ->findOne();
-
- if ($subtask) {
- $status = $this->getStatusList();
- $subtask['status_name'] = $status[$subtask['status']];
- }
-
- return $subtask;
- }
-
- return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne();
- }
-
- /**
- * Prepare data before insert/update
- *
- * @access public
- * @param array $values Form values
- */
- public function prepare(array &$values)
- {
- $this->removeFields($values, array('another_subtask'));
- $this->resetFields($values, array('time_estimated', 'time_spent'));
- }
-
- /**
- * Create
- *
- * @access public
- * @param array $values Form values
- * @return bool
- */
- public function create(array $values)
- {
- $this->prepare($values);
- $result = $this->db->table(self::TABLE)->save($values);
-
- if ($result) {
- $values['id'] = $this->db->getConnection()->getLastId();
- $this->event->trigger(self::EVENT_CREATE, $values);
- }
-
- return $result;
- }
-
- /**
- * Update
- *
- * @access public
- * @param array $values Form values
- * @return bool
- */
- public function update(array $values)
- {
- $this->prepare($values);
- $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
-
- if ($result) {
- $this->event->trigger(self::EVENT_UPDATE, $values);
- }
-
- return $result;
- }
-
- /**
- * Remove
- *
- * @access public
- * @param integer $subtask_id Subtask id
- * @return bool
- */
- public function remove($subtask_id)
- {
- return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove();
- }
-
- /**
- * Duplicate all subtasks to another task
- *
- * @access public
- * @param integer $src_task_id Source task id
- * @param integer $dst_task_id Destination task id
- * @return bool
- */
- public function duplicate($src_task_id, $dst_task_id)
- {
- $subtasks = $this->db->table(self::TABLE)
- ->columns('title', 'time_estimated')
- ->eq('task_id', $src_task_id)
- ->findAll();
-
- foreach ($subtasks as &$subtask) {
-
- $subtask['task_id'] = $dst_task_id;
- $subtask['time_spent'] = 0;
-
- if (! $this->db->table(self::TABLE)->save($subtask)) {
- return false;
- }
- }
-
- 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('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')),
- );
-
- $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', 100), 100),
- 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/Subtask.php b/app/Model/Subtask.php
new file mode 100644
index 00000000..65fa0f0c
--- /dev/null
+++ b/app/Model/Subtask.php
@@ -0,0 +1,497 @@
+<?php
+
+namespace Model;
+
+use Event\SubtaskEvent;
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Subtask model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Subtask extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'subtasks';
+
+ /**
+ * Task "done" status
+ *
+ * @var integer
+ */
+ const STATUS_DONE = 2;
+
+ /**
+ * Task "in progress" status
+ *
+ * @var integer
+ */
+ const STATUS_INPROGRESS = 1;
+
+ /**
+ * Task "todo" status
+ *
+ * @var integer
+ */
+ const STATUS_TODO = 0;
+
+ /**
+ * Events
+ *
+ * @var string
+ */
+ const EVENT_UPDATE = 'subtask.update';
+ const EVENT_CREATE = 'subtask.create';
+
+ /**
+ * Get available status
+ *
+ * @access public
+ * @return array
+ */
+ public function getStatusList()
+ {
+ return array(
+ self::STATUS_TODO => t('Todo'),
+ self::STATUS_INPROGRESS => t('In progress'),
+ self::STATUS_DONE => t('Done'),
+ );
+ }
+
+ /**
+ * Add subtask status status to the resultset
+ *
+ * @access public
+ * @param array $subtasks Subtasks
+ * @return array
+ */
+ public function addStatusName(array $subtasks)
+ {
+ $status = $this->getStatusList();
+
+ foreach ($subtasks as &$subtask) {
+ $subtask['status_name'] = $status[$subtask['status']];
+ }
+
+ return $subtasks;
+ }
+
+ /**
+ * Get the query to fetch subtasks assigned to a user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param array $status List of status
+ * @return \PicoDb\Table
+ */
+ public function getUserQuery($user_id, array $status)
+ {
+ return $this->db->table(Subtask::TABLE)
+ ->columns(
+ Subtask::TABLE.'.*',
+ Task::TABLE.'.project_id',
+ Task::TABLE.'.color_id',
+ Task::TABLE.'.title AS task_name',
+ Project::TABLE.'.name AS project_name'
+ )
+ ->eq('user_id', $user_id)
+ ->eq(Project::TABLE.'.is_active', Project::ACTIVE)
+ ->in(Subtask::TABLE.'.status', $status)
+ ->join(Task::TABLE, 'id', 'task_id')
+ ->join(Project::TABLE, 'id', 'project_id', Task::TABLE)
+ ->filter(array($this, 'addStatusName'));
+ }
+
+ /**
+ * Get all subtasks for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getAll($task_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('task_id', $task_id)
+ ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->asc(self::TABLE.'.position')
+ ->filter(array($this, 'addStatusName'))
+ ->findAll();
+ }
+
+ /**
+ * Get a subtask by the id
+ *
+ * @access public
+ * @param integer $subtask_id Subtask id
+ * @param bool $more Fetch more data
+ * @return array
+ */
+ public function getById($subtask_id, $more = false)
+ {
+ if ($more) {
+
+ return $this->db
+ ->table(self::TABLE)
+ ->eq(self::TABLE.'.id', $subtask_id)
+ ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->filter(array($this, 'addStatusName'))
+ ->findOne();
+ }
+
+ return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne();
+ }
+
+ /**
+ * Prepare data before insert/update
+ *
+ * @access public
+ * @param array $values Form values
+ */
+ public function prepare(array &$values)
+ {
+ $this->removeFields($values, array('another_subtask'));
+ $this->resetFields($values, array('time_estimated', 'time_spent'));
+ }
+
+ /**
+ * Get the position of the last column for a given project
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return integer
+ */
+ public function getLastPosition($task_id)
+ {
+ return (int) $this->db
+ ->table(self::TABLE)
+ ->eq('task_id', $task_id)
+ ->desc('position')
+ ->findOneColumn('position');
+ }
+
+ /**
+ * Create a new subtask
+ *
+ * @access public
+ * @param array $values Form values
+ * @return bool|integer
+ */
+ public function create(array $values)
+ {
+ $this->prepare($values);
+ $values['position'] = $this->getLastPosition($values['task_id']) + 1;
+
+ $subtask_id = $this->persist(self::TABLE, $values);
+
+ if ($subtask_id) {
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_CREATE,
+ new SubtaskEvent(array('id' => $subtask_id) + $values)
+ );
+ }
+
+ return $subtask_id;
+ }
+
+ /**
+ * Update
+ *
+ * @access public
+ * @param array $values Form values
+ * @return bool
+ */
+ public function update(array $values)
+ {
+ $this->prepare($values);
+ $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
+
+ if ($result) {
+
+ $this->container['dispatcher']->dispatch(
+ self::EVENT_UPDATE,
+ new SubtaskEvent($values)
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * 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 ($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
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $subtask_id
+ * @return boolean
+ */
+ public function moveDown($task_id, $subtask_id)
+ {
+ $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);
+ }
+
+ 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);
+
+ if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] > 1) {
+
+ $position = --$subtasks[$subtask_id];
+ $subtasks[$positions[$position]]++;
+
+ return $this->savePositions($subtasks);
+ }
+
+ return false;
+ }
+
+ /**
+ * Change the status of subtask
+ *
+ * Todo -> In progress -> Done -> Todo -> etc...
+ *
+ * @access public
+ * @param integer $subtask_id
+ * @return bool
+ */
+ public function toggleStatus($subtask_id)
+ {
+ $subtask = $this->getById($subtask_id);
+
+ $values = array(
+ 'id' => $subtask['id'],
+ 'status' => ($subtask['status'] + 1) % 3,
+ 'task_id' => $subtask['task_id'],
+ );
+
+ return $this->update($values);
+ }
+
+ /**
+ * Get the subtask in progress for this user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getSubtaskInProgress($user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('status', self::STATUS_INPROGRESS)
+ ->eq('user_id', $user_id)
+ ->findOne();
+ }
+
+ /**
+ * Return true if the user have a subtask in progress
+ *
+ * @access public
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function hasSubtaskInProgress($user_id)
+ {
+ return $this->config->get('subtask_restriction') == 1 &&
+ $this->db->table(self::TABLE)
+ ->eq('status', self::STATUS_INPROGRESS)
+ ->eq('user_id', $user_id)
+ ->count() === 1;
+ }
+
+ /**
+ * Remove
+ *
+ * @access public
+ * @param integer $subtask_id Subtask id
+ * @return bool
+ */
+ public function remove($subtask_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove();
+ }
+
+ /**
+ * Duplicate all subtasks to another task
+ *
+ * @access public
+ * @param integer $src_task_id Source task id
+ * @param integer $dst_task_id Destination task id
+ * @return bool
+ */
+ public function duplicate($src_task_id, $dst_task_id)
+ {
+ return $this->db->transaction(function ($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();
+
+ foreach ($subtasks as &$subtask) {
+
+ $subtask['task_id'] = $dst_task_id;
+
+ if (! $db->table(Subtask::TABLE)->save($subtask)) {
+ return false;
+ }
+ }
+ });
+ }
+
+ /**
+ * 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/SubtaskExport.php b/app/Model/SubtaskExport.php
new file mode 100644
index 00000000..23dcc019
--- /dev/null
+++ b/app/Model/SubtaskExport.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Model;
+
+/**
+ * Subtask Export
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class SubtaskExport extends Base
+{
+ /**
+ * Subtask statuses
+ *
+ * @access private
+ * @var array
+ */
+ private $subtask_status = array();
+
+ /**
+ * Fetch subtasks and return the prepared CSV
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param mixed $from Start date (timestamp or user formatted date)
+ * @param mixed $to End date (timestamp or user formatted date)
+ * @return array
+ */
+ public function export($project_id, $from, $to)
+ {
+ $this->subtask_status = $this->subtask->getStatusList();
+ $subtasks = $this->getSubtasks($project_id, $from, $to);
+ $results = array($this->getColumns());
+
+ foreach ($subtasks as $subtask) {
+ $results[] = $this->format($subtask);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Get column titles
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getColumns()
+ {
+ return array(
+ e('Subtask Id'),
+ e('Title'),
+ e('Status'),
+ e('Assignee'),
+ e('Time estimated'),
+ e('Time spent'),
+ e('Task Id'),
+ e('Task Title'),
+ );
+ }
+
+ /**
+ * Format the output of a subtask array
+ *
+ * @access public
+ * @param array $subtask Subtask properties
+ * @return array
+ */
+ public function format(array $subtask)
+ {
+ $values = array();
+ $values[] = $subtask['id'];
+ $values[] = $subtask['title'];
+ $values[] = $this->subtask_status[$subtask['status']];
+ $values[] = $subtask['assignee_name'] ?: $subtask['assignee_username'];
+ $values[] = $subtask['time_estimated'];
+ $values[] = $subtask['time_spent'];
+ $values[] = $subtask['task_id'];
+ $values[] = $subtask['task_title'];
+
+ return $values;
+ }
+
+ /**
+ * Get all subtasks for a given project
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param mixed $from Start date (timestamp or user formatted date)
+ * @param mixed $to End date (timestamp or user formatted date)
+ * @return array
+ */
+ public function getSubtasks($project_id, $from, $to)
+ {
+ if (! is_numeric($from)) {
+ $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from));
+ }
+
+ if (! is_numeric($to)) {
+ $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to)));
+ }
+
+ return $this->db->table(Subtask::TABLE)
+ ->eq('project_id', $project_id)
+ ->columns(
+ Subtask::TABLE.'.*',
+ User::TABLE.'.username AS assignee_username',
+ User::TABLE.'.name AS assignee_name',
+ Task::TABLE.'.title AS task_title'
+ )
+ ->gte('date_creation', $from)
+ ->lte('date_creation', $to)
+ ->join(Task::TABLE, 'id', 'task_id')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->asc(Subtask::TABLE.'.id')
+ ->findAll();
+ }
+}
diff --git a/app/Model/SubtaskForecast.php b/app/Model/SubtaskForecast.php
new file mode 100644
index 00000000..263aa27a
--- /dev/null
+++ b/app/Model/SubtaskForecast.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Model;
+
+use DateTime;
+use DateInterval;
+
+/**
+ * Subtask Forecast
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class SubtaskForecast extends Base
+{
+ /**
+ * Get not completed subtasks with an estimate sorted by postition
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getSubtasks($user_id)
+ {
+ return $this->db
+ ->table(Subtask::TABLE)
+ ->columns(Subtask::TABLE.'.id', Task::TABLE.'.project_id', Subtask::TABLE.'.task_id', Subtask::TABLE.'.title', Subtask::TABLE.'.time_estimated')
+ ->join(Task::TABLE, 'id', 'task_id')
+ ->asc(Task::TABLE.'.position')
+ ->asc(Subtask::TABLE.'.position')
+ ->gt(Subtask::TABLE.'.time_estimated', 0)
+ ->eq(Subtask::TABLE.'.status', Subtask::STATUS_TODO)
+ ->eq(Subtask::TABLE.'.user_id', $user_id)
+ ->findAll();
+ }
+
+ /**
+ * Get the start date for the forecast
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getStartDate($user_id)
+ {
+ $subtask = $this->db->table(Subtask::TABLE)
+ ->columns(Subtask::TABLE.'.time_estimated', SubtaskTimeTracking::TABLE.'.start')
+ ->eq(SubtaskTimeTracking::TABLE.'.user_id', $user_id)
+ ->eq(SubtaskTimeTracking::TABLE.'.end', 0)
+ ->status('status', Subtask::STATUS_INPROGRESS)
+ ->join(SubtaskTimeTracking::TABLE, 'subtask_id', 'id')
+ ->findOne();
+
+ if ($subtask && $subtask['time_estimated'] && $subtask['start']) {
+ return date('Y-m-d H:i', $subtask['start'] + $subtask['time_estimated'] * 3600);
+ }
+
+ return date('Y-m-d H:i');
+ }
+
+ /**
+ * Get all calendar events according to the user timetable and the subtasks estimates
+ *
+ * @access public
+ * @param integer $user_id
+ * @param string $end End date of the calendar
+ * @return array
+ */
+ public function getCalendarEvents($user_id, $end)
+ {
+ $events = array();
+ $start_date = new DateTime($this->getStartDate($user_id));
+ $timetable = $this->timetable->calculate($user_id, $start_date, new DateTime($end));
+ $subtasks = $this->getSubtasks($user_id);
+ $total = count($subtasks);
+ $offset = 0;
+
+ foreach ($timetable as $slot) {
+
+ $interval = $this->dateParser->getHours($slot[0], $slot[1]);
+ $start = $slot[0]->getTimestamp();
+
+ if ($slot[0] < $start_date) {
+
+ if (! $this->dateParser->withinDateRange($start_date, $slot[0], $slot[1])) {
+ continue;
+ }
+
+ $interval = $this->dateParser->getHours(new DateTime, $slot[1]);
+ $start = time();
+ }
+
+ while ($offset < $total) {
+
+ $event = array(
+ 'id' => $subtasks[$offset]['id'].'-'.$subtasks[$offset]['task_id'].'-'.$offset,
+ 'subtask_id' => $subtasks[$offset]['id'],
+ 'title' => t('#%d', $subtasks[$offset]['task_id']).' '.$subtasks[$offset]['title'],
+ 'url' => $this->helper->url->to('task', 'show', array('task_id' => $subtasks[$offset]['task_id'], 'project_id' => $subtasks[$offset]['project_id'])),
+ 'editable' => false,
+ 'start' => date('Y-m-d\TH:i:s', $start),
+ );
+
+ if ($subtasks[$offset]['time_estimated'] <= $interval) {
+
+ $start += $subtasks[$offset]['time_estimated'] * 3600;
+ $interval -= $subtasks[$offset]['time_estimated'];
+ $offset++;
+
+ $event['end'] = date('Y-m-d\TH:i:s', $start);
+ $events[] = $event;
+ }
+ else {
+ $subtasks[$offset]['time_estimated'] -= $interval;
+ $event['end'] = $slot[1]->format('Y-m-d\TH:i:s');
+ $events[] = $event;
+ break;
+ }
+ }
+ }
+
+ return $events;
+ }
+}
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
new file mode 100644
index 00000000..93a698b6
--- /dev/null
+++ b/app/Model/SubtaskTimeTracking.php
@@ -0,0 +1,340 @@
+<?php
+
+namespace Model;
+
+use DateTime;
+
+/**
+ * Subtask timesheet
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class SubtaskTimeTracking extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'subtask_time_tracking';
+
+ /**
+ * Get query for user timesheet (pagination)
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return \PicoDb\Table
+ */
+ public function getUserQuery($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.subtask_id',
+ self::TABLE.'.end',
+ self::TABLE.'.start',
+ self::TABLE.'.time_spent',
+ Subtask::TABLE.'.task_id',
+ Subtask::TABLE.'.title AS subtask_title',
+ Task::TABLE.'.title AS task_title',
+ Task::TABLE.'.project_id',
+ Task::TABLE.'.color_id'
+ )
+ ->join(Subtask::TABLE, 'id', 'subtask_id')
+ ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
+ ->eq(self::TABLE.'.user_id', $user_id);
+ }
+
+ /**
+ * Get query for task timesheet (pagination)
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return \PicoDb\Table
+ */
+ public function getTaskQuery($task_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.subtask_id',
+ self::TABLE.'.end',
+ self::TABLE.'.start',
+ self::TABLE.'.time_spent',
+ self::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ Subtask::TABLE.'.title AS subtask_title',
+ Task::TABLE.'.project_id',
+ User::TABLE.'.username',
+ User::TABLE.'.name AS user_fullname'
+ )
+ ->join(Subtask::TABLE, 'id', 'subtask_id')
+ ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', self::TABLE)
+ ->eq(Task::TABLE.'.id', $task_id);
+ }
+
+ /**
+ * Get query for project timesheet (pagination)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return \PicoDb\Table
+ */
+ public function getProjectQuery($project_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.subtask_id',
+ self::TABLE.'.end',
+ self::TABLE.'.start',
+ self::TABLE.'.time_spent',
+ self::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ Subtask::TABLE.'.title AS subtask_title',
+ Task::TABLE.'.project_id',
+ Task::TABLE.'.color_id',
+ User::TABLE.'.username',
+ User::TABLE.'.name AS user_fullname'
+ )
+ ->join(Subtask::TABLE, 'id', 'subtask_id')
+ ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', self::TABLE)
+ ->eq(Task::TABLE.'.project_id', $project_id)
+ ->asc(self::TABLE.'.id');
+ }
+
+ /**
+ * Get all recorded time slots for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getUserTimesheet($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('user_id', $user_id)
+ ->findAll();
+ }
+
+ /**
+ * Get user calendar events
+ *
+ * @access public
+ * @param integer $user_id
+ * @param integer $start
+ * @param integer $end
+ * @return array
+ */
+ public function getUserCalendarEvents($user_id, $start, $end)
+ {
+ $result = $this->getUserQuery($user_id)
+ ->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ 'start',
+ 'end'
+ ))
+ ->findAll();
+
+ $result = $this->timetable->calculateEventsIntersect($user_id, $result, $start, $end);
+
+ return $this->toCalendarEvents($result);
+ }
+
+ /**
+ * Get project calendar events
+ *
+ * @access public
+ * @param integer $project_id
+ * @param integer $start
+ * @param integer $end
+ * @return array
+ */
+ public function getProjectCalendarEvents($project_id, $start, $end)
+ {
+ $result = $this
+ ->getProjectQuery($project_id)
+ ->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ 'start',
+ 'end'
+ ))
+ ->findAll();
+
+ return $this->toCalendarEvents($result);
+ }
+
+ /**
+ * Convert a record set to calendar events
+ *
+ * @access private
+ * @param array $rows
+ * @return array
+ */
+ private function toCalendarEvents(array $rows)
+ {
+ $events = array();
+
+ foreach ($rows as $row) {
+
+ $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
+
+ $events[] = array(
+ 'id' => $row['id'],
+ 'subtask_id' => $row['subtask_id'],
+ 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
+ 'start' => date('Y-m-d\TH:i:s', $row['start']),
+ 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
+ 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
+ 'borderColor' => $this->color->getBorderColor($row['color_id']),
+ 'textColor' => 'black',
+ 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
+ 'editable' => false,
+ );
+ }
+
+ return $events;
+ }
+
+ /**
+ * Log start time
+ *
+ * @access public
+ * @param integer $subtask_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function logStartTime($subtask_id, $user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->insert(array('subtask_id' => $subtask_id, 'user_id' => $user_id, 'start' => time()));
+ }
+
+ /**
+ * Log end time
+ *
+ * @access public
+ * @param integer $subtask_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function logEndTime($subtask_id, $user_id)
+ {
+ $time_spent = $this->getTimeSpent($subtask_id, $user_id);
+
+ if ($time_spent > 0) {
+ $this->updateSubtaskTimeSpent($subtask_id, $time_spent);
+ }
+
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('subtask_id', $subtask_id)
+ ->eq('user_id', $user_id)
+ ->eq('end', 0)
+ ->update(array(
+ 'end' => time(),
+ 'time_spent' => $time_spent,
+ ));
+ }
+
+ /**
+ * Calculate the time spent when the clock is stopped
+ *
+ * @access public
+ * @param integer $subtask_id
+ * @param integer $user_id
+ * @return float
+ */
+ public function getTimeSpent($subtask_id, $user_id)
+ {
+ $start_time = $this->db
+ ->table(self::TABLE)
+ ->eq('subtask_id', $subtask_id)
+ ->eq('user_id', $user_id)
+ ->eq('end', 0)
+ ->findOneColumn('start');
+
+ if ($start_time) {
+
+ $start = new DateTime;
+ $start->setTimestamp($start_time);
+
+ return $this->timetable->calculateEffectiveDuration($user_id, $start, new DateTime);
+ }
+
+ return 0;
+ }
+
+ /**
+ * Update subtask time spent
+ *
+ * @access public
+ * @param integer $subtask_id
+ * @param float $time_spent
+ * @return bool
+ */
+ public function updateSubtaskTimeSpent($subtask_id, $time_spent)
+ {
+ $subtask = $this->subtask->getById($subtask_id);
+
+ // Fire the event subtask.update
+ return $this->subtask->update(array(
+ 'id' => $subtask['id'],
+ 'time_spent' => $subtask['time_spent'] + $time_spent,
+ 'task_id' => $subtask['task_id'],
+ ));
+ }
+
+ /**
+ * Update task time tracking based on subtasks time tracking
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return bool
+ */
+ public function updateTaskTimeTracking($task_id)
+ {
+ $result = $this->calculateSubtaskTime($task_id);
+
+ if (empty($result['total_spent']) && empty($result['total_estimated'])) {
+ return true;
+ }
+
+ return $this->db
+ ->table(Task::TABLE)
+ ->eq('id', $task_id)
+ ->update(array(
+ 'time_spent' => $result['total_spent'],
+ 'time_estimated' => $result['total_estimated'],
+ ));
+ }
+
+ /**
+ * Sum time spent and time estimated for all subtasks
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function calculateSubtaskTime($task_id)
+ {
+ return $this->db
+ ->table(Subtask::TABLE)
+ ->eq('task_id', $task_id)
+ ->columns(
+ 'SUM(time_spent) AS total_spent',
+ 'SUM(time_estimated) AS total_estimated'
+ )
+ ->findOne();
+ }
+}
diff --git a/app/Model/Swimlane.php b/app/Model/Swimlane.php
new file mode 100644
index 00000000..3b78a406
--- /dev/null
+++ b/app/Model/Swimlane.php
@@ -0,0 +1,561 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Swimlanes
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Swimlane extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'swimlanes';
+
+ /**
+ * Value for active swimlanes
+ *
+ * @var integer
+ */
+ const ACTIVE = 1;
+
+ /**
+ * Value for inactive swimlanes
+ *
+ * @var integer
+ */
+ const INACTIVE = 0;
+
+ /**
+ * Get a swimlane by the id
+ *
+ * @access public
+ * @param integer $swimlane_id Swimlane id
+ * @return array
+ */
+ public function getById($swimlane_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $swimlane_id)->findOne();
+ }
+
+ /**
+ * Get the swimlane name by the id
+ *
+ * @access public
+ * @param integer $swimlane_id Swimlane id
+ * @return string
+ */
+ public function getNameById($swimlane_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $swimlane_id)->findOneColumn('name') ?: '';
+ }
+
+ /**
+ * Get a swimlane id by the project and the name
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $name Name
+ * @return integer
+ */
+ public function getIdByName($project_id, $name)
+ {
+ return (int) $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('name', $name)
+ ->findOneColumn('id');
+ }
+
+ /**
+ * Get a swimlane by the project and the name
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $name Swimlane name
+ * @return array
+ */
+ public function getByName($project_id, $name)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('name', $name)
+ ->findOne();
+ }
+
+ /**
+ * Get default swimlane properties
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getDefault($project_id)
+ {
+ $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']);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get all swimlanes 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)
+ ->orderBy('position', 'asc')
+ ->findAll();
+ }
+
+ /**
+ * Get the list of swimlanes by status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $status Status
+ * @return array
+ */
+ public function getAllByStatus($project_id, $status = self::ACTIVE)
+ {
+ $query = $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', $status);
+
+ if ($status == self::ACTIVE) {
+ $query->asc('position');
+ }
+ else {
+ $query->asc('name');
+ }
+
+ return $query->findAll();
+ }
+
+ /**
+ * Get active swimlanes
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return array
+ */
+ public function getSwimlanes($project_id)
+ {
+ $swimlanes = $this->db->table(self::TABLE)
+ ->columns('id', 'name')
+ ->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') {
+ $default_swimlane = t($default_swimlane);
+ }
+
+ array_unshift($swimlanes, array('id' => 0, 'name' => $default_swimlane));
+ }
+
+ return $swimlanes;
+ }
+
+ /**
+ * Get list of all swimlanes
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param boolean $prepend Prepend default value
+ * @param boolean $only_active Return only active swimlanes
+ * @return array
+ */
+ public function getList($project_id, $prepend = false, $only_active = false)
+ {
+ $swimlanes = array();
+ $default = $this->db->table(Project::TABLE)->eq('id', $project_id)->eq('show_default_swimlane', 1)->findOneColumn('default_swimlane');
+
+ if ($prepend) {
+ $swimlanes[-1] = t('All swimlanes');
+ }
+
+ if (! empty($default)) {
+ $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');
+ }
+
+ /**
+ * Add a new swimlane
+ *
+ * @access public
+ * @param integer $project_id
+ * @param string $name
+ * @return integer|boolean
+ */
+ public function create($project_id, $name)
+ {
+ return $this->persist(self::TABLE, array(
+ 'project_id' => $project_id,
+ 'name' => $name,
+ 'position' => $this->getLastPosition($project_id),
+ ));
+ }
+
+ /**
+ * Rename a swimlane
+ *
+ * @access public
+ * @param integer $swimlane_id Swimlane id
+ * @param string $name Swimlane name
+ * @return bool
+ */
+ public function rename($swimlane_id, $name)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('id', $swimlane_id)
+ ->update(array('name' => $name));
+ }
+
+ /**
+ * Update the default swimlane
+ *
+ * @access public
+ * @param array $values Form values
+ * @return bool
+ */
+ 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'],
+ ));
+ }
+
+ /**
+ * Get the last position of a swimlane
+ *
+ * @access public
+ * @param integer $project_id
+ * @return integer
+ */
+ public function getLastPosition($project_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('is_active', 1)
+ ->count() + 1;
+ }
+
+ /**
+ * Disable a swimlane
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $swimlane_id Swimlane id
+ * @return bool
+ */
+ 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,
+ ));
+
+ if ($result) {
+ // Re-order positions
+ $this->updatePositions($project_id);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Enable a swimlane
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $swimlane_id Swimlane id
+ * @return bool
+ */
+ 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),
+ ));
+ }
+
+ /**
+ * Remove a swimlane
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $swimlane_id Swimlane id
+ * @return bool
+ */
+ public function remove($project_id, $swimlane_id)
+ {
+ $this->db->startTransaction();
+
+ // Tasks should not be assigned anymore to this swimlane
+ $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->update(array('swimlane_id' => 0));
+
+ if (! $this->db->table(self::TABLE)->eq('id', $swimlane_id)->remove()) {
+ $this->db->cancelTransaction();
+ return false;
+ }
+
+ // Re-order positions
+ $this->updatePositions($project_id);
+
+ $this->db->closeTransaction();
+
+ return true;
+ }
+
+ /**
+ * Update swimlane positions after disabling or removing a swimlane
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @return boolean
+ */
+ 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');
+
+ if (! $swimlanes) {
+ return false;
+ }
+
+ foreach ($swimlanes as $swimlane_id) {
+ $this->db->table(self::TABLE)
+ ->eq('id', $swimlane_id)
+ ->update(array('position' => ++$position));
+ }
+
+ return true;
+ }
+
+ /**
+ * Move a swimlane down, increment the position value
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $swimlane_id Swimlane id
+ * @return boolean
+ */
+ public function moveDown($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);
+
+ 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;
+ }
+
+ return false;
+ }
+
+ /**
+ * 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);
+
+ 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();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Duplicate Swimlane to project
+ *
+ * @access public
+ * @param integer $project_from Project Template
+ * @param integer $project_to Project that receives the copy
+ * @return integer|boolean
+ */
+
+ public function duplicate($project_from, $project_to)
+ {
+ $swimlanes = $this->getAll($project_from);
+
+ foreach ($swimlanes as $swimlane) {
+
+ unset($swimlane['id']);
+ $swimlane['project_id'] = $project_to;
+
+ if (! $this->db->table(self::TABLE)->save($swimlane)) {
+ return false;
+ }
+ }
+
+ $default_swimlane = $this->getDefault($project_from);
+ $default_swimlane['id'] = $project_to;
+
+ $this->updateDefault($default_swimlane);
+
+ 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 a0090641..71d973a4 100644
--- a/app/Model/Task.php
+++ b/app/Model/Task.php
@@ -30,212 +30,52 @@ class Task extends Base
*
* @var string
*/
+ const EVENT_MOVE_PROJECT = 'task.move.project';
const EVENT_MOVE_COLUMN = 'task.move.column';
const EVENT_MOVE_POSITION = 'task.move.position';
+ const EVENT_MOVE_SWIMLANE = 'task.move.swimlane';
const EVENT_UPDATE = 'task.update';
const EVENT_CREATE = 'task.create';
const EVENT_CLOSE = 'task.close';
const EVENT_OPEN = 'task.open';
const EVENT_CREATE_UPDATE = 'task.create_update';
const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change';
+ const EVENT_OVERDUE = 'task.overdue';
/**
- * Prepare data before task creation or modification
+ * Recurrence: status
*
- * @access public
- * @param array $values Form values
- */
- public function prepare(array &$values)
- {
- $this->dateParser->convert($values, array('date_due', 'date_started'));
- $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'));
- }
-
- /**
- * Prepare data before task creation
- *
- * @access public
- * @param array $values Form values
- */
- public function prepareCreation(array &$values)
- {
- $this->prepare($values);
-
- if (empty($values['column_id'])) {
- $values['column_id'] = $this->board->getFirstColumn($values['project_id']);
- }
-
- if (empty($values['color_id'])) {
- $colors = $this->color->getList();
- $values['color_id'] = key($colors);
- }
-
- $values['date_creation'] = time();
- $values['date_modification'] = $values['date_creation'];
- $values['position'] = $this->taskFinder->countByColumnId($values['project_id'], $values['column_id']) + 1;
- }
-
- /**
- * Prepare data before task modification
- *
- * @access public
- * @param array $values Form values
- */
- public function prepareModification(array &$values)
- {
- $this->prepare($values);
- $values['date_modification'] = time();
- }
-
- /**
- * Create a task
- *
- * @access public
- * @param array $values Form values
- * @return boolean|integer
- */
- public function create(array $values)
- {
- $this->db->startTransaction();
-
- $this->prepareCreation($values);
-
- if (! $this->db->table(self::TABLE)->save($values)) {
- $this->db->cancelTransaction();
- return false;
- }
-
- $task_id = $this->db->getConnection()->getLastId();
-
- $this->db->closeTransaction();
-
- // Trigger events
- $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
- $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values);
-
- return $task_id;
- }
-
- /**
- * Update a task
- *
- * @access public
- * @param array $values Form values
- * @param boolean $trigger_Events Trigger events
- * @return boolean
+ * @var integer
*/
- public function update(array $values, $trigger_events = true)
- {
- // Fetch original task
- $original_task = $this->taskFinder->getById($values['id']);
-
- if (! $original_task) {
- return false;
- }
-
- // Prepare data
- $updated_task = $values;
- $this->prepareModification($updated_task);
-
- $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task);
-
- if ($result && $trigger_events) {
- $this->triggerUpdateEvents($original_task, $updated_task);
- }
-
- return true;
- }
+ const RECURRING_STATUS_NONE = 0;
+ const RECURRING_STATUS_PENDING = 1;
+ const RECURRING_STATUS_PROCESSED = 2;
/**
- * Trigger events for task modification
+ * Recurrence: trigger
*
- * @access public
- * @param array $original_task Original task data
- * @param array $updated_task Updated task data
+ * @var integer
*/
- public function triggerUpdateEvents(array $original_task, array $updated_task)
- {
- $events = array();
-
- if (isset($updated_task['owner_id']) && $original_task['owner_id'] != $updated_task['owner_id']) {
- $events[] = self::EVENT_ASSIGNEE_CHANGE;
- }
- else if (isset($updated_task['column_id']) && $original_task['column_id'] != $updated_task['column_id']) {
- $events[] = self::EVENT_MOVE_COLUMN;
- }
- else if (isset($updated_task['position']) && $original_task['position'] != $updated_task['position']) {
- $events[] = self::EVENT_MOVE_POSITION;
- }
- else {
- $events[] = self::EVENT_CREATE_UPDATE;
- $events[] = self::EVENT_UPDATE;
- }
-
- $event_data = array_merge($original_task, $updated_task);
- $event_data['task_id'] = $original_task['id'];
-
- foreach ($events as $event) {
- $this->event->trigger($event, $event_data);
- }
- }
+ const RECURRING_TRIGGER_FIRST_COLUMN = 0;
+ const RECURRING_TRIGGER_LAST_COLUMN = 1;
+ const RECURRING_TRIGGER_CLOSE = 2;
/**
- * Mark a task closed
+ * Recurrence: timeframe
*
- * @access public
- * @param integer $task_id Task id
- * @return boolean
+ * @var integer
*/
- public function close($task_id)
- {
- if (! $this->taskFinder->exists($task_id)) {
- return false;
- }
-
- $result = $this->db
- ->table(self::TABLE)
- ->eq('id', $task_id)
- ->update(array(
- 'is_active' => 0,
- 'date_completed' => time()
- ));
-
- if ($result) {
- $this->event->trigger(self::EVENT_CLOSE, array('task_id' => $task_id) + $this->taskFinder->getById($task_id));
- }
-
- return $result;
- }
+ const RECURRING_TIMEFRAME_DAYS = 0;
+ const RECURRING_TIMEFRAME_MONTHS = 1;
+ const RECURRING_TIMEFRAME_YEARS = 2;
/**
- * Mark a task open
+ * Recurrence: base date used to calculate new due date
*
- * @access public
- * @param integer $task_id Task id
- * @return boolean
+ * @var integer
*/
- public function open($task_id)
- {
- if (! $this->taskFinder->exists($task_id)) {
- return false;
- }
-
- $result = $this->db
- ->table(self::TABLE)
- ->eq('id', $task_id)
- ->update(array(
- 'is_active' => 1,
- 'date_completed' => 0
- ));
-
- if ($result) {
- $this->event->trigger(self::EVENT_OPEN, array('task_id' => $task_id) + $this->taskFinder->getById($task_id));
- }
-
- return $result;
- }
+ const RECURRING_BASEDATE_DUEDATE = 0;
+ const RECURRING_BASEDATE_TRIGGERDATE = 1;
/**
* Remove a task
@@ -256,242 +96,78 @@ class Task extends Base
}
/**
- * Move a task to another column or to another position
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $task_id Task id
- * @param integer $column_id Column id
- * @param integer $position Position (must be >= 1)
- * @return boolean
- */
- public function movePosition($project_id, $task_id, $column_id, $position)
- {
- // The position can't be lower than 1
- if ($position < 1) {
- return false;
- }
-
- $board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id');
- $columns = array();
-
- // Prepare the columns
- foreach ($board as $board_column_id) {
-
- $columns[$board_column_id] = $this->db->table(self::TABLE)
- ->eq('is_active', 1)
- ->eq('project_id', $project_id)
- ->eq('column_id', $board_column_id)
- ->neq('id', $task_id)
- ->asc('position')
- ->findAllByColumn('id');
- }
-
- // The column must exists
- if (! isset($columns[$column_id])) {
- return false;
- }
-
- // We put our task to the new position
- array_splice($columns[$column_id], $position - 1, 0, $task_id); // print_r($columns);
-
- // We save the new positions for all tasks
- return $this->savePositions($task_id, $columns);
- }
-
- /**
- * Save task positions
+ * Get a the task id from a text
*
- * @access private
- * @param integer $moved_task_id Id of the moved task
- * @param array $columns Sorted tasks
- * @return boolean
- */
- private function savePositions($moved_task_id, array $columns)
- {
- $this->db->startTransaction();
-
- foreach ($columns as $column_id => $column) {
-
- $position = 1;
-
- foreach ($column as $task_id) {
-
- if ($task_id == $moved_task_id) {
-
- // Events will be triggered only for that task
- $result = $this->update(array(
- 'id' => $task_id,
- 'position' => $position,
- 'column_id' => $column_id
- ));
- }
- else {
- $result = $this->db->table(self::TABLE)->eq('id', $task_id)->update(array(
- 'position' => $position,
- 'column_id' => $column_id
- ));
- }
-
- $position++;
-
- if (! $result) {
- $this->db->cancelTransaction();
- return false;
- }
- }
- }
-
- $this->db->closeTransaction();
-
- return true;
- }
-
- /**
- * Move a task to another project
+ * Example: "Fix bug #1234" will return 1234
*
* @access public
- * @param integer $project_id Project id
- * @param array $task Task data
- * @return boolean
+ * @param string $message Text
+ * @return integer
*/
- public function moveToAnotherProject($project_id, array $task)
+ public function getTaskIdFromText($message)
{
- $values = array();
-
- // Clear values (categories are different for each project)
- $values['category_id'] = 0;
- $values['owner_id'] = 0;
-
- // Check if the assigned user is allowed for the new project
- if ($task['owner_id'] && $this->projectPermission->isUserAllowed($project_id, $task['owner_id'])) {
- $values['owner_id'] = $task['owner_id'];
- }
-
- // We use the first column of the new project
- $values['column_id'] = $this->board->getFirstColumn($project_id);
- $values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1;
- $values['project_id'] = $project_id;
-
- // The task will be open (close event binding)
- $values['is_active'] = 1;
-
- if ($this->db->table(self::TABLE)->eq('id', $task['id'])->update($values)) {
- return $task['id'];
+ if (preg_match('!#(\d+)!i', $message, $matches) && isset($matches[1])) {
+ return $matches[1];
}
- return false;
+ return 0;
}
/**
- * Generic method to duplicate a task
+ * Return the list user selectable recurrence status
*
* @access public
- * @param array $task Task data
- * @param array $override Task properties to override
- * @return integer|boolean
+ * @return array
*/
- public function copy(array $task, array $override = array())
+ public function getRecurrenceStatusList()
{
- // Values to override
- if (! empty($override)) {
- $task = $override + $task;
- }
-
- $this->db->startTransaction();
-
- // Assign new values
- $values = array();
- $values['title'] = $task['title'];
- $values['description'] = $task['description'];
- $values['date_creation'] = time();
- $values['date_modification'] = $values['date_creation'];
- $values['date_due'] = $task['date_due'];
- $values['color_id'] = $task['color_id'];
- $values['project_id'] = $task['project_id'];
- $values['column_id'] = $task['column_id'];
- $values['owner_id'] = 0;
- $values['creator_id'] = $task['creator_id'];
- $values['position'] = $this->taskFinder->countByColumnId($values['project_id'], $values['column_id']) + 1;
- $values['score'] = $task['score'];
- $values['category_id'] = 0;
-
- // Check if the assigned user is allowed for the new project
- if ($task['owner_id'] && $this->projectPermission->isUserAllowed($values['project_id'], $task['owner_id'])) {
- $values['owner_id'] = $task['owner_id'];
- }
-
- // Check if the category exists
- if ($task['category_id'] && $this->category->exists($task['category_id'], $task['project_id'])) {
- $values['category_id'] = $task['category_id'];
- }
-
- // Save task
- if (! $this->db->table(Task::TABLE)->save($values)) {
- $this->db->cancelTransaction();
- return false;
- }
-
- $task_id = $this->db->getConnection()->getLastId();
-
- // Duplicate subtasks
- if (! $this->subTask->duplicate($task['id'], $task_id)) {
- $this->db->cancelTransaction();
- return false;
- }
-
- $this->db->closeTransaction();
-
- // Trigger events
- $this->event->trigger(Task::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values);
- $this->event->trigger(Task::EVENT_CREATE, array('task_id' => $task_id) + $values);
-
- return $task_id;
+ return array (
+ Task::RECURRING_STATUS_NONE => t('No'),
+ Task::RECURRING_STATUS_PENDING => t('Yes'),
+ );
}
/**
- * Duplicate a task to the same project
+ * Return the list recurrence triggers
*
* @access public
- * @param array $task Task data
- * @return integer|boolean
+ * @return array
*/
- public function duplicateToSameProject($task)
+ public function getRecurrenceTriggerList()
{
- return $this->copy($task);
+ return array (
+ Task::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'),
+ Task::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'),
+ Task::RECURRING_TRIGGER_CLOSE => t('When task is closed'),
+ );
}
/**
- * Duplicate a task to another project (always copy to the first column)
+ * Return the list options to calculate recurrence due date
*
* @access public
- * @param integer $project_id Destination project id
- * @param array $task Task data
- * @return integer|boolean
+ * @return array
*/
- public function duplicateToAnotherProject($project_id, array $task)
+ public function getRecurrenceBasedateList()
{
- return $this->copy($task, array(
- 'project_id' => $project_id,
- 'column_id' => $this->board->getFirstColumn($project_id),
- ));
+ return array (
+ Task::RECURRING_BASEDATE_DUEDATE => t('Existing due date'),
+ Task::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'),
+ );
}
/**
- * Get a the task id from a text
- *
- * Example: "Fix bug #1234" will return 1234
+ * Return the list recurrence timeframes
*
* @access public
- * @param string $message Text
- * @return integer
+ * @return array
*/
- public function getTaskIdFromText($message)
+ public function getRecurrenceTimeframeList()
{
- if (preg_match('!#(\d+)!i', $message, $matches) && isset($matches[1])) {
- return $matches[1];
- }
-
- return 0;
+ return array (
+ Task::RECURRING_TIMEFRAME_DAYS => t('Day(s)'),
+ Task::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'),
+ Task::RECURRING_TIMEFRAME_YEARS => t('Year(s)'),
+ );
}
}
diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php
new file mode 100644
index 00000000..893cbc43
--- /dev/null
+++ b/app/Model/TaskCreation.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Model;
+
+use Event\TaskEvent;
+
+/**
+ * Task Creation
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskCreation extends Base
+{
+ /**
+ * Create a task
+ *
+ * @access public
+ * @param array $values Form values
+ * @return integer
+ */
+ public function create(array $values)
+ {
+ if (! $this->project->exists($values['project_id'])) {
+ return 0;
+ }
+
+ $this->prepare($values);
+ $task_id = $this->persist(Task::TABLE, $values);
+
+ if ($task_id !== false) {
+ $this->fireEvents($task_id, $values);
+ }
+
+ return (int) $task_id;
+ }
+
+ /**
+ * Prepare data
+ *
+ * @access public
+ * @param array $values Form values
+ */
+ public function prepare(array &$values)
+ {
+ $this->dateParser->convert($values, array('date_due', 'date_started'));
+ $this->removeFields($values, array('another_task'));
+ $this->resetFields($values, array('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']);
+ }
+
+ if (empty($values['color_id'])) {
+ $values['color_id'] = $this->color->getDefaultColor();
+ }
+
+ if (empty($values['title'])) {
+ $values['title'] = t('Untitled');
+ }
+
+ $values['swimlane_id'] = empty($values['swimlane_id']) ? 0 : $values['swimlane_id'];
+ $values['date_creation'] = time();
+ $values['date_modification'] = $values['date_creation'];
+ $values['date_moved'] = $values['date_creation'];
+ $values['position'] = $this->taskFinder->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1;
+ }
+
+ /**
+ * Fire events
+ *
+ * @access private
+ * @param integer $task_id Task id
+ * @param array $values Form values
+ */
+ private function fireEvents($task_id, array $values)
+ {
+ $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));
+ }
+}
diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php
new file mode 100755
index 00000000..afcac4c7
--- /dev/null
+++ b/app/Model/TaskDuplication.php
@@ -0,0 +1,248 @@
+<?php
+
+namespace Model;
+
+use DateTime;
+use DateInterval;
+use Event\TaskEvent;
+
+/**
+ * Task Duplication
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskDuplication extends Base
+{
+ /**
+ * Fields to copy when duplicating a task
+ *
+ * @access private
+ * @var array
+ */
+ private $fields_to_duplicate = array(
+ 'title',
+ 'description',
+ 'date_due',
+ 'color_id',
+ 'project_id',
+ 'column_id',
+ 'owner_id',
+ 'score',
+ 'category_id',
+ 'time_estimated',
+ 'swimlane_id',
+ 'recurrence_status',
+ 'recurrence_trigger',
+ 'recurrence_factor',
+ 'recurrence_timeframe',
+ 'recurrence_basedate',
+ );
+
+ /**
+ * Duplicate a task to the same project
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean|integer Duplicated task id
+ */
+ public function duplicate($task_id)
+ {
+ return $this->save($task_id, $this->copyFields($task_id));
+ }
+
+ /**
+ * Duplicate recurring task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean|integer Recurrence task id
+ */
+ public function duplicateRecurringTask($task_id)
+ {
+ $values = $this->copyFields($task_id);
+
+ if ($values['recurrence_status'] == Task::RECURRING_STATUS_PENDING) {
+
+ $values['recurrence_parent'] = $task_id;
+ $values['column_id'] = $this->board->getFirstColumn($values['project_id']);
+ $this->calculateRecurringTaskDueDate($values);
+
+ $recurring_task_id = $this->save($task_id, $values);
+
+ if ($recurring_task_id > 0) {
+
+ $parent_update = $this->db
+ ->table(Task::TABLE)
+ ->eq('id', $task_id)
+ ->update(array(
+ 'recurrence_status' => Task::RECURRING_STATUS_PROCESSED,
+ 'recurrence_child' => $recurring_task_id,
+ ));
+
+ if ($parent_update) {
+ return $recurring_task_id;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Duplicate a task to another project
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param integer $project_id Project id
+ * @return boolean|integer Duplicated task id
+ */
+ public function duplicateToProject($task_id, $project_id)
+ {
+ $values = $this->copyFields($task_id);
+ $values['project_id'] = $project_id;
+ $values['column_id'] = $this->board->getFirstColumn($project_id);
+
+ $this->checkDestinationProjectValues($values);
+
+ return $this->save($task_id, $values);
+ }
+
+ /**
+ * Move a task to another project
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param integer $project_id Project id
+ * @return boolean
+ */
+ public function moveToProject($task_id, $project_id)
+ {
+ $task = $this->taskFinder->getById($task_id);
+
+ $values = array();
+ $values['is_active'] = 1;
+ $values['project_id'] = $project_id;
+ $values['column_id'] = $this->board->getFirstColumn($project_id);
+ $values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1;
+ $values['owner_id'] = $task['owner_id'];
+ $values['category_id'] = $task['category_id'];
+ $values['swimlane_id'] = $task['swimlane_id'];
+
+ $this->checkDestinationProjectValues($values);
+
+ if ($this->db->table(Task::TABLE)->eq('id', $task['id'])->update($values)) {
+ $this->container['dispatcher']->dispatch(
+ Task::EVENT_MOVE_PROJECT,
+ new TaskEvent(array_merge($task, $values, array('task_id' => $task['id'])))
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if the assignee and the category are available in the destination project
+ *
+ * @access private
+ * @param array $values
+ */
+ private function checkDestinationProjectValues(&$values)
+ {
+ // Check if the assigned user is allowed for the destination project
+ if ($values['owner_id'] > 0 && ! $this->projectPermission->isUserAllowed($values['project_id'], $values['owner_id'])) {
+ $values['owner_id'] = 0;
+ }
+
+ // Check if the category exists for the destination project
+ if ($values['category_id'] > 0) {
+ $values['category_id'] = $this->category->getIdByName(
+ $values['project_id'],
+ $this->category->getNameById($values['category_id'])
+ );
+ }
+
+ // Check if the swimlane exists for the destination project
+ if ($values['swimlane_id'] > 0) {
+ $values['swimlane_id'] = $this->swimlane->getIdByName(
+ $values['project_id'],
+ $this->swimlane->getNameById($values['swimlane_id'])
+ );
+ }
+ }
+
+ /**
+ * Calculate new due date for new recurrence task
+ *
+ * @access public
+ * @param array $values Task fields
+ */
+ public function calculateRecurringTaskDueDate(array &$values)
+ {
+ if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) {
+
+ if ($values['recurrence_basedate'] == Task::RECURRING_BASEDATE_TRIGGERDATE) {
+ $values['date_due'] = time();
+ }
+
+ $factor = abs($values['recurrence_factor']);
+ $subtract = $values['recurrence_factor'] < 0;
+
+ switch ($values['recurrence_timeframe']) {
+ case Task::RECURRING_TIMEFRAME_MONTHS:
+ $interval = 'P' . $factor . 'M';
+ break;
+ case Task::RECURRING_TIMEFRAME_YEARS:
+ $interval = 'P' . $factor . 'Y';
+ break;
+ default:
+ $interval = 'P' . $factor . 'D';
+ }
+
+ $date_due = new DateTime();
+ $date_due->setTimestamp($values['date_due']);
+
+ $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval));
+
+ $values['date_due'] = $date_due->getTimestamp();
+ }
+ }
+
+ /**
+ * Duplicate fields for the new task
+ *
+ * @access private
+ * @param integer $task_id Task id
+ * @return array
+ */
+ private function copyFields($task_id)
+ {
+ $task = $this->taskFinder->getById($task_id);
+ $values = array();
+
+ foreach ($this->fields_to_duplicate as $field) {
+ $values[$field] = $task[$field];
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create the new task and duplicate subtasks
+ *
+ * @access private
+ * @param integer $task_id Task id
+ * @param array $values Form values
+ * @return boolean|integer
+ */
+ private function save($task_id, array $values)
+ {
+ $new_task_id = $this->taskCreation->create($values);
+
+ if ($new_task_id) {
+ $this->subtask->duplicate($task_id, $new_task_id);
+ }
+
+ return $new_task_id;
+ }
+}
diff --git a/app/Model/TaskExport.php b/app/Model/TaskExport.php
index b929823e..90aa1964 100644
--- a/app/Model/TaskExport.php
+++ b/app/Model/TaskExport.php
@@ -24,10 +24,11 @@ class TaskExport extends Base
public function export($project_id, $from, $to)
{
$tasks = $this->getTasks($project_id, $from, $to);
+ $swimlanes = $this->swimlane->getList($project_id);
$results = array($this->getColumns());
foreach ($tasks as &$task) {
- $results[] = array_values($this->format($task));
+ $results[] = array_values($this->format($task, $swimlanes));
}
return $results;
@@ -50,6 +51,7 @@ class TaskExport extends Base
projects.name AS project_name,
tasks.is_active,
project_has_categories.name AS category_name,
+ tasks.swimlane_id,
columns.title AS column_title,
tasks.position,
tasks.color_id,
@@ -71,14 +73,15 @@ class TaskExport extends Base
LEFT JOIN columns ON columns.id = tasks.column_id
LEFT JOIN projects ON projects.id = tasks.project_id
WHERE tasks.date_creation >= ? AND tasks.date_creation <= ? AND tasks.project_id = ?
+ ORDER BY tasks.id ASC
';
if (! is_numeric($from)) {
- $from = $this->dateParser->resetDateToMidnight($this->dateParser->getTimestamp($from));
+ $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from));
}
if (! is_numeric($to)) {
- $to = $this->dateParser->resetDateToMidnight(strtotime('+1 day', $this->dateParser->getTimestamp($to)));
+ $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to)));
}
$rq = $this->db->execute($sql, array($from, $to, $project_id));
@@ -89,15 +92,18 @@ class TaskExport extends Base
* Format the output of a task array
*
* @access public
- * @param array $task Task properties
+ * @param array $task Task properties
+ * @param array $swimlanes List of swimlanes
* @return array
*/
- public function format(array &$task)
+ public function format(array &$task, array &$swimlanes)
{
$colors = $this->color->getList();
$task['is_active'] = $task['is_active'] == Task::STATUS_OPEN ? e('Open') : e('Closed');
$task['color_id'] = $colors[$task['color_id']];
+ $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');
@@ -108,7 +114,7 @@ class TaskExport extends Base
* Get column titles
*
* @access public
- * @return array
+ * @return string[]
*/
public function getColumns()
{
@@ -117,6 +123,7 @@ class TaskExport extends Base
e('Project'),
e('Status'),
e('Category'),
+ e('Swimlane'),
e('Column'),
e('Position'),
e('Color'),
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
new file mode 100644
index 00000000..134f67cd
--- /dev/null
+++ b/app/Model/TaskFilter.php
@@ -0,0 +1,467 @@
+<?php
+
+namespace Model;
+
+use DateTime;
+use Eluceo\iCal\Component\Calendar;
+use Eluceo\iCal\Component\Event;
+use Eluceo\iCal\Property\Event\Attendees;
+
+/**
+ * Task Filter
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskFilter extends Base
+{
+ /**
+ * Query
+ *
+ * @access public
+ * @var \PicoDb\Table
+ */
+ public $query;
+
+ /**
+ * Create a new query
+ *
+ * @access public
+ * @return TaskFilter
+ */
+ public function create()
+ {
+ $this->query = $this->db->table(Task::TABLE);
+ $this->query->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id');
+ $this->query->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id');
+
+ $this->query->columns(
+ Task::TABLE.'.*',
+ 'ua.email AS assignee_email',
+ 'ua.username AS assignee_username',
+ 'uc.email AS creator_email',
+ 'uc.username AS creator_username'
+ );
+
+ return $this;
+ }
+
+ /**
+ * Clone the filter
+ *
+ * @access public
+ * @return TaskFilter
+ */
+ public function copy()
+ {
+ $filter = clone($this);
+ $filter->query = clone($this->query);
+ return $filter;
+ }
+
+ /**
+ * Exclude a list of task_id
+ *
+ * @access public
+ * @param array $task_ids
+ * @return TaskFilter
+ */
+ public function excludeTasks(array $task_ids)
+ {
+ $this->query->notin(Task::TABLE.'.id', $task_ids);
+ return $this;
+ }
+
+ /**
+ * Filter by id
+ *
+ * @access public
+ * @param integer $task_id
+ * @return TaskFilter
+ */
+ public function filterById($task_id)
+ {
+ if ($task_id > 0) {
+ $this->query->eq(Task::TABLE.'.id', $task_id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by title
+ *
+ * @access public
+ * @param string $title
+ * @return TaskFilter
+ */
+ public function filterByTitle($title)
+ {
+ $this->query->ilike('title', '%'.$title.'%');
+ return $this;
+ }
+
+ /**
+ * Filter by a list of project id
+ *
+ * @access public
+ * @param array $project_ids
+ * @return TaskFilter
+ */
+ public function filterByProjects(array $project_ids)
+ {
+ $this->query->in('project_id', $project_ids);
+ return $this;
+ }
+
+ /**
+ * Filter by project id
+ *
+ * @access public
+ * @param integer $project_id
+ * @return TaskFilter
+ */
+ public function filterByProject($project_id)
+ {
+ if ($project_id > 0) {
+ $this->query->eq('project_id', $project_id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by category id
+ *
+ * @access public
+ * @param integer $category_id
+ * @return TaskFilter
+ */
+ public function filterByCategory($category_id)
+ {
+ if ($category_id >= 0) {
+ $this->query->eq('category_id', $category_id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by assignee
+ *
+ * @access public
+ * @param integer $owner_id
+ * @return TaskFilter
+ */
+ public function filterByOwner($owner_id)
+ {
+ if ($owner_id >= 0) {
+ $this->query->eq('owner_id', $owner_id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by color
+ *
+ * @access public
+ * @param string $color_id
+ * @return TaskFilter
+ */
+ public function filterByColor($color_id)
+ {
+ if ($color_id !== '') {
+ $this->query->eq('color_id', $color_id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by column
+ *
+ * @access public
+ * @param integer $column_id
+ * @return TaskFilter
+ */
+ public function filterByColumn($column_id)
+ {
+ if ($column_id >= 0) {
+ $this->query->eq('column_id', $column_id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by swimlane
+ *
+ * @access public
+ * @param integer $swimlane_id
+ * @return TaskFilter
+ */
+ public function filterBySwimlane($swimlane_id)
+ {
+ if ($swimlane_id >= 0) {
+ $this->query->eq('swimlane_id', $swimlane_id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by status
+ *
+ * @access public
+ * @param integer $is_active
+ * @return TaskFilter
+ */
+ public function filterByStatus($is_active)
+ {
+ if ($is_active >= 0) {
+ $this->query->eq('is_active', $is_active);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by due date (range)
+ *
+ * @access public
+ * @param string $start
+ * @param string $end
+ * @return TaskFilter
+ */
+ public function filterByDueDateRange($start, $end)
+ {
+ $this->query->gte('date_due', $this->dateParser->getTimestampFromIsoFormat($start));
+ $this->query->lte('date_due', $this->dateParser->getTimestampFromIsoFormat($end));
+
+ return $this;
+ }
+
+ /**
+ * Filter by start date (range)
+ *
+ * @access public
+ * @param string $start
+ * @param strings $end
+ * @return TaskFilter
+ */
+ public function filterByStartDateRange($start, $end)
+ {
+ $this->query->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ 'date_started',
+ 'date_completed'
+ ));
+
+ return $this;
+ }
+
+ /**
+ * Filter by creation date
+ *
+ * @access public
+ * @param string $start
+ * @param string $end
+ * @return TaskFilter
+ */
+ public function filterByCreationDateRange($start, $end)
+ {
+ $this->query->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ 'date_creation',
+ 'date_completed'
+ ));
+
+ return $this;
+ }
+
+ /**
+ * Get all results of the filter
+ *
+ * @access public
+ * @return array
+ */
+ public function findAll()
+ {
+ return $this->query->findAll();
+ }
+
+ /**
+ * Format the results to the ajax autocompletion
+ *
+ * @access public
+ * @return array
+ */
+ public function toAutoCompletion()
+ {
+ return $this->query->columns('id', 'title')->filter(function(array $results) {
+
+ foreach ($results as &$result) {
+ $result['value'] = $result['title'];
+ $result['label'] = '#'.$result['id'].' - '.$result['title'];
+ }
+
+ return $results;
+
+ })->findAll();
+ }
+
+ /**
+ * Transform results to calendar events
+ *
+ * @access public
+ * @param string $start_column Column name for the start date
+ * @param string $end_column Column name for the end date
+ * @return array
+ */
+ public function toDateTimeCalendarEvents($start_column, $end_column)
+ {
+ $events = array();
+
+ foreach ($this->query->findAll() as $task) {
+
+ $events[] = array_merge(
+ $this->getTaskCalendarProperties($task),
+ array(
+ 'start' => date('Y-m-d\TH:i:s', $task[$start_column]),
+ 'end' => date('Y-m-d\TH:i:s', $task[$end_column] ?: time()),
+ 'editable' => false,
+ )
+ );
+ }
+
+ return $events;
+ }
+
+ /**
+ * Transform results to all day calendar events
+ *
+ * @access public
+ * @param string $column Column name for the date
+ * @return array
+ */
+ public function toAllDayCalendarEvents($column = 'date_due')
+ {
+ $events = array();
+
+ foreach ($this->query->findAll() as $task) {
+
+ $events[] = array_merge(
+ $this->getTaskCalendarProperties($task),
+ array(
+ 'start' => date('Y-m-d', $task[$column]),
+ 'end' => date('Y-m-d', $task[$column]),
+ 'allday' => true,
+ )
+ );
+ }
+
+ return $events;
+ }
+
+ /**
+ * Transform results to ical events
+ *
+ * @access public
+ * @param string $start_column Column name for the start date
+ * @param string $end_column Column name for the end date
+ * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object
+ * @return Eluceo\iCal\Component\Calendar
+ */
+ public function addDateTimeIcalEvents($start_column, $end_column, Calendar $vCalendar = null)
+ {
+ if ($vCalendar === null) {
+ $vCalendar = new Calendar('Kanboard');
+ }
+
+ foreach ($this->query->findAll() as $task) {
+
+ $start = new DateTime;
+ $start->setTimestamp($task[$start_column]);
+
+ $end = new DateTime;
+ $end->setTimestamp($task[$end_column] ?: time());
+
+ $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$start_column.'-'.$end_column);
+ $vEvent->setDtStart($start);
+ $vEvent->setDtEnd($end);
+
+ $vCalendar->addComponent($vEvent);
+ }
+
+ return $vCalendar;
+ }
+
+ /**
+ * Transform results to all day ical events
+ *
+ * @access public
+ * @param string $column Column name for the date
+ * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object
+ * @return Eluceo\iCal\Component\Calendar
+ */
+ public function addAllDayIcalEvents($column = 'date_due', Calendar $vCalendar = null)
+ {
+ if ($vCalendar === null) {
+ $vCalendar = new Calendar('Kanboard');
+ }
+
+ foreach ($this->query->findAll() as $task) {
+
+ $date = new DateTime;
+ $date->setTimestamp($task[$column]);
+
+ $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$column);
+ $vEvent->setDtStart($date);
+ $vEvent->setDtEnd($date);
+ $vEvent->setNoTime(true);
+
+ $vCalendar->addComponent($vEvent);
+ }
+
+ return $vCalendar;
+ }
+
+ /**
+ * Get common events for task ical events
+ *
+ * @access protected
+ * @param array $task
+ * @param string $uid
+ * @return Eluceo\iCal\Component\Event
+ */
+ protected function getTaskIcalEvent(array &$task, $uid)
+ {
+ $dateCreation = new DateTime;
+ $dateCreation->setTimestamp($task['date_creation']);
+
+ $dateModif = new DateTime;
+ $dateModif->setTimestamp($task['date_modification']);
+
+ $vEvent = new Event($uid);
+ $vEvent->setCreated($dateCreation);
+ $vEvent->setModified($dateModif);
+ $vEvent->setUseTimezone(true);
+ $vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']);
+ $vEvent->setUrl($this->helper->url->base().$this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])));
+
+ if (! empty($task['creator_id'])) {
+ $vEvent->setOrganizer('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local'));
+ }
+
+ if (! empty($task['owner_id'])) {
+ $attendees = new Attendees;
+ $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local'));
+ $vEvent->setAttendees($attendees);
+ }
+
+ return $vEvent;
+ }
+}
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index 56795152..5a1d33c6 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -13,20 +13,78 @@ use PDO;
class TaskFinder extends Base
{
/**
- * Common request to fetch a list of tasks
+ * Get query for closed tasks
*
- * @access private
+ * @access public
+ * @param integer $project_id Project id
+ * @return \PicoDb\Table
+ */
+ public function getClosedTaskQuery($project_id)
+ {
+ return $this->getExtendedQuery()
+ ->eq('project_id', $project_id)
+ ->eq('is_active', Task::STATUS_CLOSED);
+ }
+
+ /**
+ * Get query for task search
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $search Search terms
* @return \PicoDb\Table
*/
- private function prepareRequestList()
+ public function getSearchQuery($project_id, $search)
+ {
+ return $this->getExtendedQuery()
+ ->eq('project_id', $project_id)
+ ->ilike('title', '%'.$search.'%');
+ }
+
+ /**
+ * Get query for assigned user tasks
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return \PicoDb\Table
+ */
+ public function getUserQuery($user_id)
+ {
+ return $this->db
+ ->table(Task::TABLE)
+ ->columns(
+ 'tasks.id',
+ 'tasks.title',
+ 'tasks.date_due',
+ 'tasks.date_creation',
+ 'tasks.project_id',
+ 'tasks.color_id',
+ 'tasks.time_spent',
+ 'tasks.time_estimated',
+ 'projects.name AS project_name'
+ )
+ ->join(Project::TABLE, 'id', 'project_id')
+ ->eq(Task::TABLE.'.owner_id', $user_id)
+ ->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN)
+ ->eq(Project::TABLE.'.is_active', Project::ACTIVE);
+ }
+
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
{
return $this->db
->table(Task::TABLE)
->columns(
- '(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments',
- '(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files',
- '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id) AS nb_subtasks',
- '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(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',
'tasks.id',
'tasks.reference',
'tasks.title',
@@ -38,12 +96,21 @@ class TaskFinder extends Base
'tasks.color_id',
'tasks.project_id',
'tasks.column_id',
+ 'tasks.swimlane_id',
'tasks.owner_id',
'tasks.creator_id',
'tasks.position',
'tasks.is_active',
'tasks.score',
'tasks.category_id',
+ 'tasks.date_moved',
+ 'tasks.recurrence_status',
+ 'tasks.recurrence_trigger',
+ 'tasks.recurrence_factor',
+ 'tasks.recurrence_timeframe',
+ 'tasks.recurrence_basedate',
+ 'tasks.recurrence_parent',
+ 'tasks.recurrence_child',
'users.username AS assignee_username',
'users.name AS assignee_name'
)
@@ -51,94 +118,26 @@ class TaskFinder extends Base
}
/**
- * Task search with pagination
- *
- * @access public
- * @param integer $project_id Project id
- * @param string $search Search terms
- * @param integer $offset Offset
- * @param integer $limit Limit
- * @param string $column Sorting column
- * @param string $direction Sorting direction
- * @return array
- */
- public function search($project_id, $search, $offset = 0, $limit = 25, $column = 'tasks.id', $direction = 'DESC')
- {
- return $this->prepareRequestList()
- ->eq('project_id', $project_id)
- ->like('title', '%'.$search.'%')
- ->offset($offset)
- ->limit($limit)
- ->orderBy($column, $direction)
- ->findAll();
- }
-
- /**
- * Get all completed tasks with pagination
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $offset Offset
- * @param integer $limit Limit
- * @param string $column Sorting column
- * @param string $direction Sorting direction
- * @return array
- */
- public function getClosedTasks($project_id, $offset = 0, $limit = 25, $column = 'tasks.date_completed', $direction = 'DESC')
- {
- return $this->prepareRequestList()
- ->eq('project_id', $project_id)
- ->eq('is_active', Task::STATUS_CLOSED)
- ->offset($offset)
- ->limit($limit)
- ->orderBy($column, $direction)
- ->findAll();
- }
-
- /**
* Get all tasks shown on the board (sorted by position)
*
* @access public
- * @param integer $project_id Project id
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param integer $swimlane_id Swimlane id
* @return array
*/
- public function getTasksOnBoard($project_id)
+ public function getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id = 0)
{
- return $this->prepareRequestList()
+ return $this->getExtendedQuery()
->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->eq('swimlane_id', $swimlane_id)
->eq('is_active', Task::STATUS_OPEN)
->asc('tasks.position')
->findAll();
}
/**
- * Get all open tasks for a given user
- *
- * @access public
- * @param integer $user_id User id
- * @return array
- */
- public function getAllTasksByUser($user_id)
- {
- return $this->db
- ->table(Task::TABLE)
- ->columns(
- 'tasks.id',
- 'tasks.title',
- 'tasks.date_due',
- 'tasks.date_creation',
- 'tasks.project_id',
- 'tasks.color_id',
- 'projects.name AS project_name'
- )
- ->join(Project::TABLE, 'id', 'project_id')
- ->eq('tasks.owner_id', $user_id)
- ->eq('tasks.is_active', Task::STATUS_OPEN)
- ->asc('tasks.id')
- ->findAll();
- }
-
- /**
* Get all tasks for a given project and status
*
* @access public
@@ -169,6 +168,8 @@ class TaskFinder extends Base
Task::TABLE.'.title',
Task::TABLE.'.date_due',
Task::TABLE.'.project_id',
+ Task::TABLE.'.creator_id',
+ Task::TABLE.'.owner_id',
Project::TABLE.'.name AS project_name',
User::TABLE.'.username AS assignee_username',
User::TABLE.'.name AS assignee_name'
@@ -185,6 +186,18 @@ class TaskFinder extends Base
}
/**
+ * Get project id for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return integer
+ */
+ public function getProjectId($task_id)
+ {
+ return (int) $this->db->table(Task::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0;
+ }
+
+ /**
* Fetch a task by the id
*
* @access public
@@ -200,12 +213,13 @@ class TaskFinder extends Base
* Fetch a task by the reference (external id)
*
* @access public
+ * @param integer $project_id Project id
* @param string $reference Task reference
* @return array
*/
- public function getByReference($reference)
+ public function getByReference($project_id, $reference)
{
- return $this->db->table(Task::TABLE)->eq('reference', $reference)->findOne();
+ return $this->db->table(Task::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne();
}
/**
@@ -239,6 +253,15 @@ class TaskFinder extends Base
tasks.is_active,
tasks.score,
tasks.category_id,
+ tasks.swimlane_id,
+ tasks.date_moved,
+ tasks.recurrence_status,
+ tasks.recurrence_trigger,
+ tasks.recurrence_factor,
+ tasks.recurrence_timeframe,
+ tasks.recurrence_basedate,
+ tasks.recurrence_parent,
+ tasks.recurrence_child,
project_has_categories.name AS category_name,
projects.name AS project_name,
columns.title AS column_title,
@@ -282,33 +305,36 @@ class TaskFinder extends Base
* @access public
* @param integer $project_id Project id
* @param integer $column_id Column id
- * @param array $status List of status id
* @return integer
*/
- public function countByColumnId($project_id, $column_id, array $status = array(Task::STATUS_OPEN))
+ public function countByColumnId($project_id, $column_id)
{
return $this->db
->table(Task::TABLE)
->eq('project_id', $project_id)
->eq('column_id', $column_id)
- ->in('is_active', $status)
+ ->in('is_active', 1)
->count();
}
/**
- * Count the number of tasks for a custom search
+ * Count the number of tasks for a given column and swimlane
*
* @access public
- * @param integer $project_id Project id
- * @param string $search Search terms
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param integer $swimlane_id Swimlane id
* @return integer
*/
- public function countSearch($project_id, $search)
+ public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id)
{
- return $this->db->table(Task::TABLE)
- ->eq('project_id', $project_id)
- ->like('title', '%'.$search.'%')
- ->count();
+ return $this->db
+ ->table(Task::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->eq('swimlane_id', $swimlane_id)
+ ->in('is_active', 1)
+ ->count();
}
/**
diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php
new file mode 100644
index 00000000..251460c9
--- /dev/null
+++ b/app/Model/TaskLink.php
@@ -0,0 +1,273 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+use PicoDb\Table;
+
+/**
+ * TaskLink model
+ *
+ * @package model
+ * @author Olivier Maridat
+ * @author Frederic Guillot
+ */
+class TaskLink extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_links';
+
+ /**
+ * Get a task link
+ *
+ * @access public
+ * @param integer $task_link_id Task link id
+ * @return array
+ */
+ public function getById($task_link_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $task_link_id)->findOne();
+ }
+
+ /**
+ * Get the opposite task link (use the unique index task_has_links_unique)
+ *
+ * @access public
+ * @param array $task_link
+ * @return array
+ */
+ public function getOppositeTaskLink(array $task_link)
+ {
+ $opposite_link_id = $this->link->getOppositeLinkId($task_link['link_id']);
+
+ return $this->db->table(self::TABLE)
+ ->eq('opposite_task_id', $task_link['task_id'])
+ ->eq('task_id', $task_link['opposite_task_id'])
+ ->eq('link_id', $opposite_link_id)
+ ->findOne();
+ }
+
+ /**
+ * Get all links attached to a task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getAll($task_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ self::TABLE.'.id',
+ self::TABLE.'.opposite_task_id AS task_id',
+ Link::TABLE.'.label',
+ Task::TABLE.'.title',
+ Task::TABLE.'.is_active',
+ Task::TABLE.'.project_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'
+ )
+ ->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(User::TABLE, 'id', 'owner_id', Task::TABLE)
+ ->orderBy(Link::TABLE.'.id ASC, '.Board::TABLE.'.position ASC, '.Task::TABLE.'.is_active DESC, '.Task::TABLE.'.id', Table::SORT_ASC)
+ ->findAll();
+ }
+
+ /**
+ * Get all links attached to a task grouped by label
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getAllGroupedByLabel($task_id)
+ {
+ $links = $this->getAll($task_id);
+ $result = array();
+
+ foreach ($links as $link) {
+
+ if (! isset($result[$link['label']])) {
+ $result[$link['label']] = array();
+ }
+
+ $result[$link['label']][] = $link;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Create a new link
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @param integer $opposite_task_id Opposite task id
+ * @param integer $link_id Link id
+ * @return integer Task link id
+ */
+ public function create($task_id, $opposite_task_id, $link_id)
+ {
+ $this->db->startTransaction();
+
+ // Get opposite link
+ $opposite_link_id = $this->link->getOppositeLinkId($link_id);
+
+ // Create the original task link
+ $this->db->table(self::TABLE)->insert(array(
+ 'task_id' => $task_id,
+ 'opposite_task_id' => $opposite_task_id,
+ 'link_id' => $link_id,
+ ));
+
+ $task_link_id = $this->db->getConnection()->getLastId();
+
+ // Create the opposite task link
+ $this->db->table(self::TABLE)->insert(array(
+ 'task_id' => $opposite_task_id,
+ 'opposite_task_id' => $task_id,
+ 'link_id' => $opposite_link_id,
+ ));
+
+ $this->db->closeTransaction();
+
+ return (int) $task_link_id;
+ }
+
+ /**
+ * Update a task link
+ *
+ * @access public
+ * @param integer $task_link_id Task link id
+ * @param integer $task_id Task id
+ * @param integer $opposite_task_id Opposite task id
+ * @param integer $link_id Link id
+ * @return boolean
+ */
+ public function update($task_link_id, $task_id, $opposite_task_id, $link_id)
+ {
+ $this->db->startTransaction();
+
+ // Get original task link
+ $task_link = $this->getById($task_link_id);
+
+ // Find opposite task link
+ $opposite_task_link = $this->getOppositeTaskLink($task_link);
+
+ // Get opposite link
+ $opposite_link_id = $this->link->getOppositeLinkId($link_id);
+
+ // Update the original task link
+ $rs1 = $this->db->table(self::TABLE)->eq('id', $task_link_id)->update(array(
+ 'task_id' => $task_id,
+ 'opposite_task_id' => $opposite_task_id,
+ 'link_id' => $link_id,
+ ));
+
+ // Update the opposite link
+ $rs2 = $this->db->table(self::TABLE)->eq('id', $opposite_task_link['id'])->update(array(
+ 'task_id' => $opposite_task_id,
+ 'opposite_task_id' => $task_id,
+ 'link_id' => $opposite_link_id,
+ ));
+
+ $this->db->closeTransaction();
+
+ return $rs1 && $rs2;
+ }
+
+ /**
+ * Remove a link between two tasks
+ *
+ * @access public
+ * @param integer $task_link_id
+ * @return boolean
+ */
+ public function remove($task_link_id)
+ {
+ $this->db->startTransaction();
+
+ $link = $this->getById($task_link_id);
+ $link_id = $this->link->getOppositeLinkId($link['link_id']);
+
+ $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove();
+
+ $this->db
+ ->table(self::TABLE)
+ ->eq('opposite_task_id', $link['task_id'])
+ ->eq('task_id', $link['opposite_task_id'])
+ ->eq('link_id', $link_id)->remove();
+
+ $this->db->closeTransaction();
+
+ 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
new file mode 100644
index 00000000..677fcd60
--- /dev/null
+++ b/app/Model/TaskModification.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Model;
+
+use Event\TaskEvent;
+
+/**
+ * Task Modification
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskModification extends Base
+{
+ /**
+ * Update a task
+ *
+ * @access public
+ * @param array $values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ $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) {
+ $this->fireEvents($original_task, $values);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fire events
+ *
+ * @access public
+ * @param array $task
+ * @param array $new_values
+ */
+ public function fireEvents(array $task, array $new_values)
+ {
+ $event_data = array_merge($task, $new_values, array('task_id' => $task['id']));
+
+ if (isset($new_values['owner_id']) && $task['owner_id'] != $new_values['owner_id']) {
+ $events = array(Task::EVENT_ASSIGNEE_CHANGE);
+ }
+ else {
+ $events = array(Task::EVENT_CREATE_UPDATE, Task::EVENT_UPDATE);
+ }
+
+ foreach ($events as $event) {
+ $this->container['dispatcher']->dispatch($event, new TaskEvent($event_data));
+ }
+ }
+
+ /**
+ * Prepare data before task modification
+ *
+ * @access public
+ * @param array $values Form values
+ */
+ public function prepare(array &$values)
+ {
+ $this->dateParser->convert($values, array('date_due', 'date_started'));
+ $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'));
+
+ $values['date_modification'] = time();
+ }
+}
diff --git a/app/Model/TaskPermission.php b/app/Model/TaskPermission.php
index 2ab154f4..e2420e10 100644
--- a/app/Model/TaskPermission.php
+++ b/app/Model/TaskPermission.php
@@ -20,10 +20,10 @@ class TaskPermission extends Base
*/
public function canRemoveTask(array $task)
{
- if ($this->acl->isAdminUser()) {
+ if ($this->userSession->isAdmin() || $this->projectPermission->isManager($task['project_id'], $this->userSession->getId())) {
return true;
}
- else if (isset($task['creator_id']) && $task['creator_id'] == $this->acl->getUserId()) {
+ else if (isset($task['creator_id']) && $task['creator_id'] == $this->userSession->getId()) {
return true;
}
diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php
new file mode 100644
index 00000000..0c4beb2d
--- /dev/null
+++ b/app/Model/TaskPosition.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Model;
+
+use Event\TaskEvent;
+
+/**
+ * Task Position
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskPosition extends Base
+{
+ /**
+ * Move a task to another column or to another position
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param integer $column_id Column id
+ * @param integer $position Position (must be >= 1)
+ * @param integer $swimlane_id Swimlane id
+ * @param boolean $fire_events Fire events
+ * @return boolean
+ */
+ public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true)
+ {
+ $original_task = $this->taskFinder->getById($task_id);
+
+ $result = $this->calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id);
+
+ if ($result) {
+
+ if ($original_task['swimlane_id'] != $swimlane_id) {
+ $this->calculateAndSave($project_id, 0, $column_id, 1, $original_task['swimlane_id']);
+ }
+
+ if ($fire_events) {
+ $this->fireEvents($original_task, $column_id, $position, $swimlane_id);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Calculate the new position of all tasks
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param integer $column_id Column id
+ * @param integer $position Position (must be >= 1)
+ * @param integer $swimlane_id Swimlane id
+ * @return array|boolean
+ */
+ public function calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id = 0)
+ {
+ // The position can't be lower than 1
+ if ($position < 1) {
+ return false;
+ }
+
+ $board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id');
+ $columns = array();
+
+ // For each column fetch all tasks ordered by position
+ foreach ($board as $board_column_id) {
+
+ $columns[$board_column_id] = $this->db->table(Task::TABLE)
+ ->eq('is_active', 1)
+ ->eq('swimlane_id', $swimlane_id)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $board_column_id)
+ ->neq('id', $task_id)
+ ->asc('position')
+ ->asc('id') // Fix Postgresql unit test
+ ->findAllByColumn('id');
+ }
+
+ // The column must exists
+ if (! isset($columns[$column_id])) {
+ return false;
+ }
+
+ // We put our task to the new position
+ if ($task_id) {
+ array_splice($columns[$column_id], $position - 1, 0, $task_id);
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Save task positions
+ *
+ * @access private
+ * @param array $columns Sorted tasks
+ * @param integer $swimlane_id Swimlane id
+ * @return boolean
+ */
+ private function savePositions(array $columns, $swimlane_id)
+ {
+ return $this->db->transaction(function ($db) use ($columns, $swimlane_id) {
+
+ foreach ($columns as $column_id => $column) {
+
+ $position = 1;
+
+ foreach ($column as $task_id) {
+
+ $result = $db->table(Task::TABLE)->eq('id', $task_id)->update(array(
+ 'position' => $position,
+ 'column_id' => $column_id,
+ 'swimlane_id' => $swimlane_id,
+ ));
+
+ if (! $result) {
+ return false;
+ }
+
+ $position++;
+ }
+ }
+ });
+ }
+
+ /**
+ * Fire events
+ *
+ * @access private
+ * @param array $task
+ * @param integer $new_column_id
+ * @param integer $new_position
+ * @param integer $new_swimlane_id
+ */
+ private function fireEvents(array $task, $new_column_id, $new_position, $new_swimlane_id)
+ {
+ $event_data = array(
+ 'task_id' => $task['id'],
+ 'project_id' => $task['project_id'],
+ 'position' => $new_position,
+ 'column_id' => $new_column_id,
+ 'swimlane_id' => $new_swimlane_id,
+ 'src_column_id' => $task['column_id'],
+ 'dst_column_id' => $new_column_id,
+ 'date_moved' => $task['date_moved'],
+ 'recurrence_status' => $task['recurrence_status'],
+ 'recurrence_trigger' => $task['recurrence_trigger'],
+ );
+
+ if ($task['swimlane_id'] != $new_swimlane_id) {
+ $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data));
+ }
+ else if ($task['column_id'] != $new_column_id) {
+ $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data));
+ }
+ else if ($task['position'] != $new_position) {
+ $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data));
+ }
+ }
+
+ /**
+ * Calculate the new position of all tasks
+ *
+ * @access private
+ * @param integer $project_id Project id
+ * @param integer $task_id Task id
+ * @param integer $column_id Column id
+ * @param integer $position Position (must be >= 1)
+ * @param integer $swimlane_id Swimlane id
+ * @return boolean
+ */
+ private function calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id)
+ {
+ $positions = $this->calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id);
+
+ if ($positions === false || ! $this->savePositions($positions, $swimlane_id)) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/app/Model/TaskStatus.php b/app/Model/TaskStatus.php
new file mode 100644
index 00000000..30a65e1e
--- /dev/null
+++ b/app/Model/TaskStatus.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Model;
+
+use Event\TaskEvent;
+
+/**
+ * Task Status
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TaskStatus extends Base
+{
+ /**
+ * Return the list of statuses
+ *
+ * @access public
+ * @param boolean $prepend Prepend default value
+ * @return array
+ */
+ public function getList($prepend = false)
+ {
+ $listing = $prepend ? array(-1 => t('All status')) : array();
+
+ return $listing + array(
+ Task::STATUS_OPEN => t('Open'),
+ Task::STATUS_CLOSED => t('Closed'),
+ );
+ }
+
+ /**
+ * Return true if the task is closed
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function isClosed($task_id)
+ {
+ return $this->checkStatus($task_id, Task::STATUS_CLOSED);
+ }
+
+ /**
+ * Return true if the task is open
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function isOpen($task_id)
+ {
+ return $this->checkStatus($task_id, Task::STATUS_OPEN);
+ }
+
+ /**
+ * Mark a task closed
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function close($task_id)
+ {
+ return $this->changeStatus($task_id, Task::STATUS_CLOSED, time(), Task::EVENT_CLOSE);
+ }
+
+ /**
+ * Mark a task open
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function open($task_id)
+ {
+ return $this->changeStatus($task_id, Task::STATUS_OPEN, 0, Task::EVENT_OPEN);
+ }
+
+ /**
+ * Common method to change the status of task
+ *
+ * @access private
+ * @param integer $task_id Task id
+ * @param integer $status Task status
+ * @param integer $date_completed Timestamp
+ * @param string $event Event name
+ * @return boolean
+ */
+ private function changeStatus($task_id, $status, $date_completed, $event)
+ {
+ if (! $this->taskFinder->exists($task_id)) {
+ return false;
+ }
+
+ $result = $this->db
+ ->table(Task::TABLE)
+ ->eq('id', $task_id)
+ ->update(array(
+ 'is_active' => $status,
+ 'date_completed' => $date_completed,
+ 'date_modification' => time(),
+ ));
+
+ if ($result) {
+ $this->container['dispatcher']->dispatch(
+ $event,
+ new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id))
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Check the status of task
+ *
+ * @access private
+ * @param integer $task_id Task id
+ * @param integer $status Task status
+ * @return boolean
+ */
+ private function checkStatus($task_id, $status)
+ {
+ return $this->db
+ ->table(Task::TABLE)
+ ->eq('id', $task_id)
+ ->eq('is_active', $status)
+ ->count() === 1;
+ }
+}
diff --git a/app/Model/TaskValidator.php b/app/Model/TaskValidator.php
index 816008cf..ec1383ad 100644
--- a/app/Model/TaskValidator.php
+++ b/app/Model/TaskValidator.php
@@ -29,6 +29,14 @@ class TaskValidator extends Base
new Validators\Integer('creator_id', t('This value must be an integer')),
new Validators\Integer('score', t('This value must be an integer')),
new Validators\Integer('category_id', t('This value must be an integer')),
+ new Validators\Integer('swimlane_id', t('This value must be an integer')),
+ new Validators\Integer('recurrence_child', t('This value must be an integer')),
+ new Validators\Integer('recurrence_parent', t('This value must be an integer')),
+ new Validators\Integer('recurrence_factor', t('This value must be an integer')),
+ new Validators\Integer('recurrence_timeframe', t('This value must be an integer')),
+ 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\MaxLength('title', t('The maximum length is %d characters', 200), 200),
new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()),
new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateFormats()),
@@ -70,7 +78,6 @@ class TaskValidator extends Base
{
$rules = array(
new Validators\Required('id', t('The id is required')),
- new Validators\Required('description', t('The description is required')),
);
$v = new Validator($values, array_merge($rules, $this->commonValidationRules()));
@@ -82,6 +89,28 @@ class TaskValidator extends Base
}
/**
+ * Validate edit recurrence
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateEditRecurrence(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()
+ );
+ }
+
+
+ /**
* Validate task modification (form)
*
* @access public
diff --git a/app/Model/TimeTracking.php b/app/Model/TimeTracking.php
deleted file mode 100644
index 4ddddf12..00000000
--- a/app/Model/TimeTracking.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-namespace Model;
-
-/**
- * Time tracking model
- *
- * @package model
- * @author Frederic Guillot
- */
-class TimeTracking extends Base
-{
- /**
- * Calculate time metrics for a task
- *
- * Use subtasks time metrics if not empty otherwise return task time metrics
- *
- * @access public
- * @param array $task Task properties
- * @param array $subtasks Subtasks list
- * @return array
- */
- public function getTaskTimesheet(array $task, array $subtasks)
- {
- $timesheet = array(
- 'time_spent' => 0,
- 'time_estimated' => 0,
- 'time_remaining' => 0,
- );
-
- foreach ($subtasks as &$subtask) {
- $timesheet['time_estimated'] += $subtask['time_estimated'];
- $timesheet['time_spent'] += $subtask['time_spent'];
- }
-
- if ($timesheet['time_estimated'] == 0 && $timesheet['time_spent'] == 0) {
- $timesheet['time_estimated'] = $task['time_estimated'];
- $timesheet['time_spent'] = $task['time_spent'];
- }
-
- $timesheet['time_remaining'] = $timesheet['time_estimated'] - $timesheet['time_spent'];
-
- return $timesheet;
- }
-}
diff --git a/app/Model/Timetable.php b/app/Model/Timetable.php
new file mode 100644
index 00000000..6ddf826b
--- /dev/null
+++ b/app/Model/Timetable.php
@@ -0,0 +1,356 @@
+<?php
+
+namespace Model;
+
+use DateTime;
+use DateInterval;
+
+/**
+ * Timetable
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Timetable extends Base
+{
+ /**
+ * User time slots
+ *
+ * @access private
+ * @var array
+ */
+ private $day;
+ private $week;
+ private $overtime;
+ private $timeoff;
+
+ /**
+ * Get a set of events by using the intersection between the timetable and the time tracking data
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $events Time tracking data
+ * @param string $start ISO8601 date
+ * @param string $end ISO8601 date
+ * @return array
+ */
+ public function calculateEventsIntersect($user_id, array $events, $start, $end)
+ {
+ $start_dt = new DateTime($start);
+ $start_dt->setTime(0, 0);
+
+ $end_dt = new DateTime($end);
+ $end_dt->setTime(23, 59);
+
+ $timetable = $this->calculate($user_id, $start_dt, $end_dt);
+
+ // The user has no timetable
+ if (empty($this->week)) {
+ return $events;
+ }
+
+ $results = array();
+
+ foreach ($events as $event) {
+ $results = array_merge($results, $this->calculateEventIntersect($event, $timetable));
+ }
+
+ return $results;
+ }
+
+ /**
+ * Get a serie of events based on the timetable and the provided event
+ *
+ * @access public
+ * @param array $event
+ * @param array $timetable
+ * @return array
+ */
+ public function calculateEventIntersect(array $event, array $timetable)
+ {
+ $events = array();
+
+ foreach ($timetable as $slot) {
+
+ $start_ts = $slot[0]->getTimestamp();
+ $end_ts = $slot[1]->getTimestamp();
+
+ if ($start_ts > $event['end']) {
+ break;
+ }
+
+ if ($event['start'] <= $start_ts) {
+ $event['start'] = $start_ts;
+ }
+
+ if ($event['start'] >= $start_ts && $event['start'] <= $end_ts) {
+
+ if ($event['end'] >= $end_ts) {
+ $events[] = array_merge($event, array('end' => $end_ts));
+ }
+ else {
+ $events[] = $event;
+ break;
+ }
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Calculate effective worked hours by taking into consideration the timetable
+ *
+ * @access public
+ * @param integer $user_id
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return float
+ */
+ public function calculateEffectiveDuration($user_id, DateTime $start, DateTime $end)
+ {
+ $end_timetable = clone($end);
+ $end_timetable->setTime(23, 59);
+
+ $timetable = $this->calculate($user_id, $start, $end_timetable);
+ $found_start = false;
+ $hours = 0;
+
+ // The user has no timetable
+ if (empty($this->week)) {
+ return $this->dateParser->getHours($start, $end);
+ }
+
+ foreach ($timetable as $slot) {
+
+ $isStartSlot = $this->dateParser->withinDateRange($start, $slot[0], $slot[1]);
+ $isEndSlot = $this->dateParser->withinDateRange($end, $slot[0], $slot[1]);
+
+ // Start and end are within the same time slot
+ if ($isStartSlot && $isEndSlot) {
+ return $this->dateParser->getHours($start, $end);
+ }
+
+ // We found the start slot
+ if (! $found_start && $isStartSlot) {
+ $found_start = true;
+ $hours = $this->dateParser->getHours($start, $slot[1]);
+ }
+ else if ($found_start) {
+
+ // We found the end slot
+ if ($isEndSlot) {
+ $hours += $this->dateParser->getHours($slot[0], $end);
+ break;
+ }
+ else {
+
+ // Sum hours of the intermediate time slots
+ $hours += $this->dateParser->getHours($slot[0], $slot[1]);
+ }
+ }
+ }
+
+ // The start date was not found in regular hours so we get the nearest time slot
+ if (! empty($timetable) && ! $found_start) {
+ $slot = $this->findClosestTimeSlot($start, $timetable);
+
+ if ($start < $slot[0]) {
+ return $this->calculateEffectiveDuration($user_id, $slot[0], $end);
+ }
+ }
+
+ return $hours;
+ }
+
+ /**
+ * Find the nearest time slot
+ *
+ * @access public
+ * @param DateTime $date
+ * @param array $timetable
+ * @return array
+ */
+ public function findClosestTimeSlot(DateTime $date, array $timetable)
+ {
+ $values = array();
+
+ foreach ($timetable as $slot) {
+ $t1 = abs($slot[0]->getTimestamp() - $date->getTimestamp());
+ $t2 = abs($slot[1]->getTimestamp() - $date->getTimestamp());
+
+ $values[] = min($t1, $t2);
+ }
+
+ asort($values);
+ return $timetable[key($values)];
+ }
+
+ /**
+ * Get the timetable for a user for a given date range
+ *
+ * @access public
+ * @param integer $user_id
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return array
+ */
+ public function calculate($user_id, DateTime $start, DateTime $end)
+ {
+ $timetable = array();
+
+ $this->day = $this->timetableDay->getByUser($user_id);
+ $this->week = $this->timetableWeek->getByUser($user_id);
+ $this->overtime = $this->timetableExtra->getByUserAndDate($user_id, $start->format('Y-m-d'), $end->format('Y-m-d'));
+ $this->timeoff = $this->timetableOff->getByUserAndDate($user_id, $start->format('Y-m-d'), $end->format('Y-m-d'));
+
+ for ($today = clone($start); $today <= $end; $today->add(new DateInterval('P1D'))) {
+ $week_day = $today->format('N');
+ $timetable = array_merge($timetable, $this->getWeekSlots($today, $week_day));
+ $timetable = array_merge($timetable, $this->getOvertimeSlots($today, $week_day));
+ }
+
+ return $timetable;
+ }
+
+ /**
+ * Return worked time slots for the given day
+ *
+ * @access public
+ * @param \DateTime $today
+ * @param string $week_day
+ * @return array
+ */
+ public function getWeekSlots(DateTime $today, $week_day)
+ {
+ $slots = array();
+ $dayoff = $this->getDayOff($today);
+
+ if (! empty($dayoff) && $dayoff['all_day'] == 1) {
+ return array();
+ }
+
+ foreach ($this->week as $slot) {
+ if ($week_day == $slot['day']) {
+ $slots = array_merge($slots, $this->getDayWorkSlots($slot, $dayoff, $today));
+ }
+ }
+
+ return $slots;
+ }
+
+ /**
+ * Get the overtime time slots for the given day
+ *
+ * @access public
+ * @param \DateTime $today
+ * @param string $week_day
+ * @return array
+ */
+ public function getOvertimeSlots(DateTime $today, $week_day)
+ {
+ $slots = array();
+
+ foreach ($this->overtime as $slot) {
+
+ $day = new DateTime($slot['date']);
+
+ if ($week_day == $day->format('N')) {
+
+ if ($slot['all_day'] == 1) {
+ $slots = array_merge($slots, $this->getDaySlots($today));
+ }
+ else {
+ $slots[] = $this->getTimeSlot($slot, $day);
+ }
+ }
+ }
+
+ return $slots;
+ }
+
+ /**
+ * Get worked time slots and remove time off
+ *
+ * @access public
+ * @param array $slot
+ * @param array $dayoff
+ * @param \DateTime $today
+ * @return array
+ */
+ public function getDayWorkSlots(array $slot, array $dayoff, DateTime $today)
+ {
+ $slots = array();
+
+ if (! empty($dayoff) && $dayoff['start'] < $slot['end']) {
+
+ if ($dayoff['start'] > $slot['start']) {
+ $slots[] = $this->getTimeSlot(array('end' => $dayoff['start']) + $slot, $today);
+ }
+
+ if ($dayoff['end'] < $slot['end']) {
+ $slots[] = $this->getTimeSlot(array('start' => $dayoff['end']) + $slot, $today);
+ }
+ }
+ else {
+ $slots[] = $this->getTimeSlot($slot, $today);
+ }
+
+ return $slots;
+ }
+
+ /**
+ * Get regular day work time slots
+ *
+ * @access public
+ * @param \DateTime $today
+ * @return array
+ */
+ public function getDaySlots(DateTime $today)
+ {
+ $slots = array();
+
+ foreach ($this->day as $day) {
+ $slots[] = $this->getTimeSlot($day, $today);
+ }
+
+ return $slots;
+ }
+
+ /**
+ * Get the start and end time slot for a given day
+ *
+ * @access public
+ * @param array $slot
+ * @param \DateTime $today
+ * @return array
+ */
+ public function getTimeSlot(array $slot, DateTime $today)
+ {
+ $date = $today->format('Y-m-d');
+
+ return array(
+ new DateTime($date.' '.$slot['start']),
+ new DateTime($date.' '.$slot['end']),
+ );
+ }
+
+ /**
+ * Return day off time slot
+ *
+ * @access public
+ * @param \DateTime $today
+ * @return array
+ */
+ public function getDayOff(DateTime $today)
+ {
+ foreach ($this->timeoff as $day) {
+
+ if ($day['date'] === $today->format('Y-m-d')) {
+ return $day;
+ }
+ }
+
+ return array();
+ }
+}
diff --git a/app/Model/TimetableDay.php b/app/Model/TimetableDay.php
new file mode 100644
index 00000000..0c7bf20b
--- /dev/null
+++ b/app/Model/TimetableDay.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Timetable Workweek
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TimetableDay extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'timetable_day';
+
+ /**
+ * Get the timetable for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getByUser($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('start')->findAll();
+ }
+
+ /**
+ * Add a new time slot in the database
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param string $start Start hour (24h format)
+ * @param string $end End hour (24h format)
+ * @return boolean|integer
+ */
+ public function create($user_id, $start, $end)
+ {
+ $values = array(
+ 'user_id' => $user_id,
+ 'start' => $start,
+ 'end' => $end,
+ );
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Remove a specific time slot
+ *
+ * @access public
+ * @param integer $slot_id
+ * @return boolean
+ */
+ public function remove($slot_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $slot_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('user_id', t('Field required')),
+ new Validators\Required('start', t('Field required')),
+ new Validators\Required('end', t('Field required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/TimetableExtra.php b/app/Model/TimetableExtra.php
new file mode 100644
index 00000000..48db662d
--- /dev/null
+++ b/app/Model/TimetableExtra.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Timetable over-time
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TimetableExtra extends TimetableOff
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'timetable_extra';
+}
diff --git a/app/Model/TimetableOff.php b/app/Model/TimetableOff.php
new file mode 100644
index 00000000..e4fe32d2
--- /dev/null
+++ b/app/Model/TimetableOff.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Timetable time off
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TimetableOff extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'timetable_off';
+
+ /**
+ * Get query to fetch everything (pagination)
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return \PicoDb\Table
+ */
+ public function getUserQuery($user_id)
+ {
+ return $this->db->table(static::TABLE)->eq('user_id', $user_id);
+ }
+
+ /**
+ * Get the timetable for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getByUser($user_id)
+ {
+ return $this->db->table(static::TABLE)->eq('user_id', $user_id)->desc('date')->asc('start')->findAll();
+ }
+
+ /**
+ * Get the timetable for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param string $start_date
+ * @param string $end_date
+ * @return array
+ */
+ public function getByUserAndDate($user_id, $start_date, $end_date)
+ {
+ return $this->db->table(static::TABLE)
+ ->eq('user_id', $user_id)
+ ->gte('date', $start_date)
+ ->lte('date', $end_date)
+ ->desc('date')
+ ->asc('start')
+ ->findAll();
+ }
+
+ /**
+ * Add a new time slot in the database
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param string $date Day (ISO8601 format)
+ * @param boolean $all_day All day flag
+ * @param float $start Start hour (24h format)
+ * @param float $end End hour (24h format)
+ * @param string $comment
+ * @return boolean|integer
+ */
+ public function create($user_id, $date, $all_day, $start = '', $end = '', $comment = '')
+ {
+ $values = array(
+ 'user_id' => $user_id,
+ 'date' => $date,
+ 'all_day' => (int) $all_day, // Postgres fix
+ 'start' => $all_day ? '' : $start,
+ 'end' => $all_day ? '' : $end,
+ 'comment' => $comment,
+ );
+
+ return $this->persist(static::TABLE, $values);
+ }
+
+ /**
+ * Remove a specific time slot
+ *
+ * @access public
+ * @param integer $slot_id
+ * @return boolean
+ */
+ public function remove($slot_id)
+ {
+ return $this->db->table(static::TABLE)->eq('id', $slot_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('user_id', t('Field required')),
+ new Validators\Required('date', t('Field required')),
+ new Validators\Numeric('all_day', t('This value must be numeric')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/TimetableWeek.php b/app/Model/TimetableWeek.php
new file mode 100644
index 00000000..b22b3b7e
--- /dev/null
+++ b/app/Model/TimetableWeek.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Timetable Workweek
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class TimetableWeek extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'timetable_week';
+
+ /**
+ * Get the timetable for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getByUser($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('day')->asc('start')->findAll();
+ }
+
+ /**
+ * Add a new time slot in the database
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param string $day Day of the week (ISO-8601)
+ * @param string $start Start hour (24h format)
+ * @param string $end End hour (24h format)
+ * @return boolean|integer
+ */
+ public function create($user_id, $day, $start, $end)
+ {
+ $values = array(
+ 'user_id' => $user_id,
+ 'day' => $day,
+ 'start' => $start,
+ 'end' => $end,
+ );
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Remove a specific time slot
+ *
+ * @access public
+ * @param integer $slot_id
+ * @return boolean
+ */
+ public function remove($slot_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $slot_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('user_id', t('Field required')),
+ new Validators\Required('day', t('Field required')),
+ new Validators\Numeric('day', t('This value must be numeric')),
+ new Validators\Required('start', t('Field required')),
+ new Validators\Required('end', t('Field required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Model/Transition.php b/app/Model/Transition.php
new file mode 100644
index 00000000..cb759e4a
--- /dev/null
+++ b/app/Model/Transition.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace Model;
+
+/**
+ * Transition model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class Transition extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'transitions';
+
+ /**
+ * Save transition event
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $task
+ * @return boolean
+ */
+ public function save($user_id, array $task)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'user_id' => $user_id,
+ 'project_id' => $task['project_id'],
+ 'task_id' => $task['task_id'],
+ 'src_column_id' => $task['src_column_id'],
+ 'dst_column_id' => $task['dst_column_id'],
+ 'date' => time(),
+ 'time_spent' => time() - $task['date_moved']
+ ));
+ }
+
+ /**
+ * Get all transitions by task
+ *
+ * @access public
+ * @param integer $task_id
+ * @return array
+ */
+ public function getAllByTask($task_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(
+ 'src.title as src_column',
+ 'dst.title as dst_column',
+ User::TABLE.'.name',
+ User::TABLE.'.username',
+ self::TABLE.'.user_id',
+ self::TABLE.'.date',
+ self::TABLE.'.time_spent'
+ )
+ ->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')
+ ->findAll();
+ }
+
+ /**
+ * Get all transitions by project
+ *
+ * @access public
+ * @param integer $project_id
+ * @param mixed $from Start date (timestamp or user formatted date)
+ * @param mixed $to End date (timestamp or user formatted date)
+ * @return array
+ */
+ public function getAllByProjectAndDate($project_id, $from, $to)
+ {
+ if (! is_numeric($from)) {
+ $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from));
+ }
+
+ if (! is_numeric($to)) {
+ $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to)));
+ }
+
+ return $this->db->table(self::TABLE)
+ ->columns(
+ Task::TABLE.'.id',
+ Task::TABLE.'.title',
+ 'src.title as src_column',
+ 'dst.title as dst_column',
+ User::TABLE.'.name',
+ User::TABLE.'.username',
+ self::TABLE.'.user_id',
+ self::TABLE.'.date',
+ self::TABLE.'.time_spent'
+ )
+ ->gte('date', $from)
+ ->lte('date', $to)
+ ->eq(self::TABLE.'.project_id', $project_id)
+ ->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')
+ ->findAll();
+ }
+
+ /**
+ * Get project export
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param mixed $from Start date (timestamp or user formatted date)
+ * @param mixed $to End date (timestamp or user formatted date)
+ * @return array
+ */
+ public function export($project_id, $from, $to)
+ {
+ $results = array($this->getColumns());
+ $transitions = $this->getAllByProjectAndDate($project_id, $from, $to);
+
+ foreach ($transitions as $transition) {
+ $results[] = $this->format($transition);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Get column titles
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getColumns()
+ {
+ return array(
+ e('Id'),
+ e('Task Title'),
+ e('Source column'),
+ e('Destination column'),
+ e('Executer'),
+ e('Date'),
+ e('Time spent'),
+ );
+ }
+
+ /**
+ * Format the output of a transition array
+ *
+ * @access public
+ * @param array $transition
+ * @return array
+ */
+ public function format(array $transition)
+ {
+ $values = array();
+ $values[] = $transition['id'];
+ $values[] = $transition['title'];
+ $values[] = $transition['src_column'];
+ $values[] = $transition['dst_column'];
+ $values[] = $transition['name'] ?: $transition['username'];
+ $values[] = date('Y-m-d H:i', $transition['date']);
+ $values[] = round($transition['time_spent'] / 3600, 2);
+
+ return $values;
+ }
+}
diff --git a/app/Model/User.php b/app/Model/User.php
index 9544f3c9..83cf065b 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -5,6 +5,7 @@ namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use Core\Session;
+use Core\Security;
/**
* User model
@@ -29,54 +30,68 @@ class User extends Base
const EVERYBODY_ID = -1;
/**
- * Return true is the given user id is administrator
+ * Return true if the user exists
*
* @access public
- * @param integer $user_id User id
+ * @param integer $user_id User id
* @return boolean
*/
- public function isAdmin($user_id)
+ public function exists($user_id)
{
- $result = $this->db
- ->table(User::TABLE)
- ->eq('id', $user_id)
- ->eq('is_admin', 1)
- ->count();
-
- return $result > 0;
+ return $this->db->table(self::TABLE)->eq('id', $user_id)->count() === 1;
}
/**
- * Get the default project from the session
+ * Get query to fetch all users
*
* @access public
- * @return integer
+ * @return \PicoDb\Table
*/
- public function getFavoriteProjectId()
+ public function getQuery()
{
- return isset($_SESSION['user']['default_project_id']) ? $_SESSION['user']['default_project_id'] : 0;
+ return $this->db
+ ->table(self::TABLE)
+ ->columns(
+ 'id',
+ 'username',
+ 'name',
+ 'email',
+ 'is_admin',
+ 'default_project_id',
+ 'is_ldap_user',
+ 'notifications_enabled',
+ 'google_id',
+ 'github_id',
+ 'twofactor_activated'
+ );
}
/**
- * Get the last seen project from the session
+ * Return the full name
*
- * @access public
- * @return integer
+ * @param array $user User properties
+ * @return string
*/
- public function getLastSeenProjectId()
+ public function getFullname(array $user)
{
- return empty($_SESSION['user']['last_show_project_id']) ? 0 : $_SESSION['user']['last_show_project_id'];
+ return $user['name'] ?: $user['username'];
}
/**
- * Set the last seen project from the session
+ * Return true is the given user id is administrator
*
* @access public
- * @@param integer $project_id Project id
+ * @param integer $user_id User id
+ * @return boolean
*/
- public function storeLastSeenProjectId($project_id)
+ public function isAdmin($user_id)
{
- $_SESSION['user']['last_show_project_id'] = (int) $project_id;
+ return $this->userSession->isAdmin() || // Avoid SQL query if connected
+ $this->db
+ ->table(User::TABLE)
+ ->eq('id', $user_id)
+ ->eq('is_admin', 1)
+ ->count() === 1;
}
/**
@@ -96,10 +111,14 @@ class User extends Base
*
* @access public
* @param string $google_id Google unique id
- * @return array
+ * @return array|boolean
*/
public function getByGoogleId($google_id)
{
+ if (empty($google_id)) {
+ return false;
+ }
+
return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne();
}
@@ -108,10 +127,14 @@ class User extends Base
*
* @access public
* @param string $github_id GitHub user id
- * @return array
+ * @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();
}
@@ -128,6 +151,38 @@ class User extends Base
}
/**
+ * Get a specific user by the email address
+ *
+ * @access public
+ * @param string $email Email
+ * @return array|boolean
+ */
+ public function getByEmail($email)
+ {
+ if (empty($email)) {
+ return false;
+ }
+
+ return $this->db->table(self::TABLE)->eq('email', $email)->findOne();
+ }
+
+ /**
+ * Fetch user by using the token
+ *
+ * @access public
+ * @param string $token Token
+ * @return array|boolean
+ */
+ public function getByToken($token)
+ {
+ if (empty($token)) {
+ return false;
+ }
+
+ return $this->db->table(self::TABLE)->eq('token', $token)->findOne();
+ }
+
+ /**
* Get all users
*
* @access public
@@ -135,11 +190,18 @@ class User extends Base
*/
public function getAll()
{
- return $this->db
- ->table(self::TABLE)
- ->asc('username')
- ->columns('id', 'username', 'name', 'email', 'is_admin', 'default_project_id', 'is_ldap_user', 'notifications_enabled', 'google_id', 'github_id')
- ->findAll();
+ return $this->getQuery()->asc('username')->findAll();
+ }
+
+ /**
+ * Get the number of users
+ *
+ * @access public
+ * @return integer
+ */
+ public function count()
+ {
+ return $this->db->table(self::TABLE)->count();
}
/**
@@ -201,12 +263,12 @@ class User extends Base
*
* @access public
* @param array $values Form values
- * @return boolean
+ * @return boolean|integer
*/
public function create(array $values)
{
$this->prepare($values);
- return $this->db->table(self::TABLE)->save($values);
+ return $this->persist(self::TABLE, $values);
}
/**
@@ -222,8 +284,8 @@ 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() && $_SESSION['user']['id'] == $values['id']) {
- $this->updateSession();
+ if (Session::isOpen() && $this->userSession->getId() == $values['id']) {
+ $this->userSession->refresh();
}
return $result;
@@ -238,39 +300,59 @@ class User extends Base
*/
public function remove($user_id)
{
- $this->db->startTransaction();
+ return $this->db->transaction(function ($db) use ($user_id) {
- // All tasks assigned to this user will be unassigned
- $this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0));
- $result = $this->db->table(self::TABLE)->eq('id', $user_id)->remove();
+ // All assigned tasks are now unassigned
+ if (! $db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) {
+ return false;
+ }
- $this->db->closeTransaction();
+ // 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');
- return $result;
+ if (! empty($project_ids)) {
+ $db->table(Project::TABLE)->in('id', $project_ids)->remove();
+ }
+
+ // Finally remove the user
+ if (! $db->table(User::TABLE)->eq('id', $user_id)->remove()) {
+ return false;
+ }
+ });
}
/**
- * Update user session information
+ * Enable public access for a user
*
* @access public
- * @param array $user User data
+ * @param integer $user_id User id
+ * @return bool
*/
- public function updateSession(array $user = array())
+ public function enablePublicAccess($user_id)
{
- if (empty($user)) {
- $user = $this->getById($_SESSION['user']['id']);
- }
-
- if (isset($user['password'])) {
- unset($user['password']);
- }
-
- $user['id'] = (int) $user['id'];
- $user['default_project_id'] = (int) $user['default_project_id'];
- $user['is_admin'] = (bool) $user['is_admin'];
- $user['is_ldap_user'] = (bool) $user['is_ldap_user'];
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $user_id)
+ ->save(array('token' => Security::generateToken()));
+ }
- $_SESSION['user'] = $user;
+ /**
+ * Disable public access for a user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return bool
+ */
+ public function disablePublicAccess($user_id)
+ {
+ return $this->db
+ ->table(self::TABLE)
+ ->eq('id', $user_id)
+ ->save(array('token' => ''));
}
/**
@@ -389,7 +471,7 @@ class User extends Base
if ($v->execute()) {
// Check password
- if ($this->authentication->authenticate($_SESSION['user']['username'], $values['current_password'])) {
+ if ($this->authentication->authenticate($this->session['user']['username'], $values['current_password'])) {
return array(true, array());
}
else {
diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php
new file mode 100644
index 00000000..6703a1bc
--- /dev/null
+++ b/app/Model/UserSession.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Model;
+
+use Core\Translator;
+
+/**
+ * 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['default_project_id'] = (int) $user['default_project_id'];
+ $user['is_admin'] = (bool) $user['is_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;
+ }
+
+ /**
+ * 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 the last seen project from the session
+ *
+ * @access public
+ * @return integer
+ */
+ public function getLastSeenProjectId()
+ {
+ return empty($this->session['last_show_project_id']) ? 0 : $this->session['last_show_project_id'];
+ }
+
+ /**
+ * Get the default project from the session
+ *
+ * @access public
+ * @return integer
+ */
+ public function getFavoriteProjectId()
+ {
+ return isset($this->session['user']['default_project_id']) ? $this->session['user']['default_project_id'] : 0;
+ }
+
+ /**
+ * Set the last seen project from the session
+ *
+ * @access public
+ * @param integer $project_id Project id
+ */
+ public function storeLastSeenProjectId($project_id)
+ {
+ $this->session['last_show_project_id'] = (int) $project_id;
+ }
+}
diff --git a/app/Model/Webhook.php b/app/Model/Webhook.php
index b84728cf..e3af37f7 100644
--- a/app/Model/Webhook.php
+++ b/app/Model/Webhook.php
@@ -2,8 +2,6 @@
namespace Model;
-use Event\WebhookListener;
-
/**
* Webhook model
*
@@ -13,139 +11,26 @@ use Event\WebhookListener;
class Webhook extends Base
{
/**
- * HTTP connection timeout in seconds
- *
- * @var integer
- */
- const HTTP_TIMEOUT = 1;
-
- /**
- * Number of maximum redirections for the HTTP client
- *
- * @var integer
- */
- const HTTP_MAX_REDIRECTS = 3;
-
- /**
- * HTTP client user agent
- *
- * @var string
- */
- const HTTP_USER_AGENT = 'Kanboard Webhook';
-
- /**
- * URL to call for task creation
- *
- * @access private
- * @var string
- */
- private $url_task_creation = '';
-
- /**
- * URL to call for task modification
- *
- * @access private
- * @var string
- */
- private $url_task_modification = '';
-
- /**
- * Webook token
- *
- * @access private
- * @var string
- */
- private $token = '';
-
- /**
- * Attach events
- *
- * @access public
- */
- public function attachEvents()
- {
- $this->url_task_creation = $this->config->get('webhook_url_task_creation');
- $this->url_task_modification = $this->config->get('webhook_url_task_modification');
- $this->token = $this->config->get('webhook_token');
-
- if ($this->url_task_creation) {
- $this->attachCreateEvents();
- }
-
- if ($this->url_task_modification) {
- $this->attachUpdateEvents();
- }
- }
-
- /**
- * Attach events for task modification
- *
- * @access public
- */
- public function attachUpdateEvents()
- {
- $events = array(
- Task::EVENT_UPDATE,
- Task::EVENT_CLOSE,
- Task::EVENT_OPEN,
- Task::EVENT_MOVE_COLUMN,
- Task::EVENT_MOVE_POSITION,
- Task::EVENT_ASSIGNEE_CHANGE,
- );
-
- $listener = new WebhookListener($this->registry);
- $listener->setUrl($this->url_task_modification);
-
- foreach ($events as $event_name) {
- $this->event->attach($event_name, $listener);
- }
- }
-
- /**
- * Attach events for task creation
- *
- * @access public
- */
- public function attachCreateEvents()
- {
- $listener = new WebhookListener($this->registry);
- $listener->setUrl($this->url_task_creation);
-
- $this->event->attach(Task::EVENT_CREATE, $listener);
- }
-
- /**
* Call the external URL
*
* @access public
- * @param string $url URL to call
- * @param array $task Task data
+ * @param array $values Event payload
*/
- public function notify($url, array $task)
+ public function notify(array $values)
{
- $headers = array(
- 'Connection: close',
- 'User-Agent: '.self::HTTP_USER_AGENT,
- );
+ $url = $this->config->get('webhook_url');
+ $token = $this->config->get('webhook_token');
- $context = stream_context_create(array(
- 'http' => array(
- 'method' => 'POST',
- 'protocol_version' => 1.1,
- 'timeout' => self::HTTP_TIMEOUT,
- 'max_redirects' => self::HTTP_MAX_REDIRECTS,
- 'header' => implode("\r\n", $headers),
- 'content' => json_encode($task)
- )
- ));
+ if (! empty($url)) {
- if (strpos($url, '?') !== false) {
- $url .= '&token='.$this->token;
- }
- else {
- $url .= '?token='.$this->token;
- }
+ if (strpos($url, '?') !== false) {
+ $url .= '&token='.$token;
+ }
+ else {
+ $url .= '?token='.$token;
+ }
- @file_get_contents($url, false, $context);
+ return $this->httpClient->postJson($url, $values);
+ }
}
}
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 4f74f761..bcb365bd 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -4,8 +4,487 @@ namespace Schema;
use PDO;
use Core\Security;
+use Model\Link;
-const VERSION = 34;
+const VERSION = 73;
+
+function version_73($pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN notifications_filter INT DEFAULT 4");
+}
+
+function version_72($pdo)
+{
+ $pdo->exec('ALTER TABLE files MODIFY name VARCHAR(255)');
+}
+
+function version_71($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO `settings` VALUES (?, ?)');
+ $rq->execute(array('webhook_url', ''));
+
+ $pdo->exec("DELETE FROM `settings` WHERE `option`='webhook_url_task_creation'");
+ $pdo->exec("DELETE FROM `settings` WHERE `option`='webhook_url_task_modification'");
+}
+
+function version_70($pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN token VARCHAR(255) DEFAULT ''");
+}
+
+function version_69($pdo)
+{
+ $rq = $pdo->prepare("SELECT `value` FROM `settings` WHERE `option`='subtask_forecast'");
+ $rq->execute();
+ $result = $rq->fetch(PDO::FETCH_ASSOC);
+
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0));
+ $rq->execute(array('calendar_user_subtasks_time_tracking', 0));
+ $rq->execute(array('calendar_user_tasks', 'date_started'));
+ $rq->execute(array('calendar_project_tasks', 'date_started'));
+
+ $pdo->exec("DELETE FROM `settings` WHERE `option`='subtask_forecast'");
+}
+
+function version_68($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_jabber', '0'));
+ $rq->execute(array('integration_jabber_server', ''));
+ $rq->execute(array('integration_jabber_domain', ''));
+ $rq->execute(array('integration_jabber_username', ''));
+ $rq->execute(array('integration_jabber_password', ''));
+ $rq->execute(array('integration_jabber_nickname', 'kanboard'));
+ $rq->execute(array('integration_jabber_room', ''));
+
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber INTEGER DEFAULT '0'");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_server VARCHAR(255) DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_domain VARCHAR(255) DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_username VARCHAR(255) DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_password VARCHAR(255) DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_nickname VARCHAR(255) DEFAULT 'kanboard'");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_room VARCHAR(255) DEFAULT ''");
+}
+
+function version_67($pdo)
+{
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER');
+}
+
+function version_66($pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN identifier VARCHAR(50) DEFAULT ''");
+}
+
+function version_65($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_integrations (
+ `id` INT NOT NULL AUTO_INCREMENT,
+ `project_id` INT NOT NULL UNIQUE,
+ `hipchat` TINYINT(1) DEFAULT 0,
+ `hipchat_api_url` VARCHAR(255) DEFAULT 'https://api.hipchat.com',
+ `hipchat_room_id` VARCHAR(255),
+ `hipchat_room_token` VARCHAR(255),
+ `slack` TINYINT(1) DEFAULT 0,
+ `slack_webhook_url` VARCHAR(255),
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
+
+function version_64($pdo)
+{
+ $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INT NOT NULL DEFAULT 0');
+}
+
+function version_63($pdo)
+{
+ $pdo->exec('ALTER TABLE project_has_categories ADD COLUMN description TEXT');
+}
+
+function version_62($pdo)
+{
+ $pdo->exec('ALTER TABLE files ADD COLUMN `date` INT NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE files ADD COLUMN `user_id` INT NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE files ADD COLUMN `size` INT NOT NULL DEFAULT 0');
+}
+
+function version_61($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated TINYINT(1) DEFAULT 0');
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret CHAR(16)');
+}
+
+function version_60($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_gravatar', '0'));
+}
+
+function version_59($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_hipchat', '0'));
+ $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com'));
+ $rq->execute(array('integration_hipchat_room_id', ''));
+ $rq->execute(array('integration_hipchat_room_token', ''));
+}
+
+function version_58($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_slack_webhook', '0'));
+ $rq->execute(array('integration_slack_webhook_url', ''));
+}
+
+function version_57($pdo)
+{
+ $pdo->exec('CREATE TABLE currencies (`currency` CHAR(3) NOT NULL UNIQUE, `rate` FLOAT DEFAULT 0) ENGINE=InnoDB CHARSET=utf8');
+
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('application_currency', 'USD'));
+}
+
+function version_56($pdo)
+{
+ $pdo->exec('CREATE TABLE transitions (
+ `id` INT NOT NULL AUTO_INCREMENT,
+ `user_id` INT NOT NULL,
+ `project_id` INT NOT NULL,
+ `task_id` INT NOT NULL,
+ `src_column_id` INT NOT NULL,
+ `dst_column_id` INT NOT NULL,
+ `date` INT NOT NULL,
+ `time_spent` INT DEFAULT 0,
+ FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8');
+
+ $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)");
+ $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)");
+ $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)");
+}
+
+function version_55($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('subtask_forecast', '0'));
+}
+
+function version_54($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('application_stylesheet', ''));
+}
+
+function version_53($pdo)
+{
+ $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent FLOAT DEFAULT 0");
+}
+
+function version_52($pdo)
+{
+ $pdo->exec('CREATE TABLE budget_lines (
+ `id` INT NOT NULL AUTO_INCREMENT,
+ `project_id` INT NOT NULL,
+ `amount` FLOAT NOT NULL,
+ `date` VARCHAR(10) NOT NULL,
+ `comment` TEXT,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8');
+}
+
+function version_51($pdo)
+{
+ $pdo->exec('CREATE TABLE timetable_day (
+ id INT NOT NULL AUTO_INCREMENT,
+ user_id INT NOT NULL,
+ start VARCHAR(5) NOT NULL,
+ end VARCHAR(5) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8');
+
+ $pdo->exec('CREATE TABLE timetable_week (
+ id INT NOT NULL AUTO_INCREMENT,
+ user_id INTEGER NOT NULL,
+ day INT NOT NULL,
+ start VARCHAR(5) NOT NULL,
+ end VARCHAR(5) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8');
+
+ $pdo->exec('CREATE TABLE timetable_off (
+ id INT NOT NULL AUTO_INCREMENT,
+ user_id INT NOT NULL,
+ date VARCHAR(10) NOT NULL,
+ all_day TINYINT(1) DEFAULT 0,
+ start VARCHAR(5) DEFAULT 0,
+ end VARCHAR(5) DEFAULT 0,
+ comment TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8');
+
+ $pdo->exec('CREATE TABLE timetable_extra (
+ id INT NOT NULL AUTO_INCREMENT,
+ user_id INT NOT NULL,
+ date VARCHAR(10) NOT NULL,
+ all_day TINYINT(1) DEFAULT 0,
+ start VARCHAR(5) DEFAULT 0,
+ end VARCHAR(5) DEFAULT 0,
+ comment TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8');
+}
+
+function version_50($pdo)
+{
+ $pdo->exec("CREATE TABLE hourly_rates (
+ id INT NOT NULL AUTO_INCREMENT,
+ user_id INT NOT NULL,
+ rate FLOAT DEFAULT 0,
+ date_effective INTEGER NOT NULL,
+ currency CHAR(3) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8");
+}
+
+function version_49($pdo)
+{
+ $pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1');
+
+ $task_id = 0;
+ $position = 1;
+ $urq = $pdo->prepare('UPDATE subtasks SET position=? WHERE id=?');
+
+ $rq = $pdo->prepare('SELECT * FROM subtasks ORDER BY task_id, id ASC');
+ $rq->execute();
+
+ foreach ($rq->fetchAll(PDO::FETCH_ASSOC) as $subtask) {
+
+ if ($task_id != $subtask['task_id']) {
+ $position = 1;
+ $task_id = $subtask['task_id'];
+ }
+
+ $urq->execute(array($position, $subtask['id']));
+ $position++;
+ }
+}
+
+function version_48($pdo)
+{
+ $pdo->exec('RENAME TABLE task_has_files TO files');
+ $pdo->exec('RENAME TABLE task_has_subtasks TO subtasks');
+}
+
+function version_47($pdo)
+{
+ $pdo->exec('ALTER TABLE projects ADD COLUMN description TEXT');
+}
+
+function version_46($pdo)
+{
+ $pdo->exec("CREATE TABLE links (
+ id INT NOT NULL AUTO_INCREMENT,
+ label VARCHAR(255) NOT NULL,
+ opposite_id INT DEFAULT 0,
+ PRIMARY KEY(id),
+ UNIQUE(label)
+ ) ENGINE=InnoDB CHARSET=utf8");
+
+ $pdo->exec("CREATE TABLE task_has_links (
+ id INT NOT NULL AUTO_INCREMENT,
+ link_id INT NOT NULL,
+ task_id INT NOT NULL,
+ opposite_task_id INT NOT NULL,
+ FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8");
+
+ $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)");
+ $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)");
+
+ $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)');
+ $rq->execute(array('relates to', 0));
+ $rq->execute(array('blocks', 3));
+ $rq->execute(array('is blocked by', 2));
+ $rq->execute(array('duplicates', 5));
+ $rq->execute(array('is duplicated by', 4));
+ $rq->execute(array('is a child of', 7));
+ $rq->execute(array('is a parent of', 6));
+ $rq->execute(array('targets milestone', 9));
+ $rq->execute(array('is a milestone of', 8));
+ $rq->execute(array('fixes', 11));
+ $rq->execute(array('is fixed by', 10));
+}
+
+function version_45($pdo)
+{
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INT DEFAULT 0');
+
+ /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0.
+ * We take max project_activities.date_creation where event_name in task.create','task.move.column
+ * since creation date is always less than task moves
+ */
+ $pdo->exec("UPDATE tasks
+ SET date_moved = (
+ SELECT md
+ FROM (
+ SELECT task_id, max(date_creation) md
+ FROM project_activities
+ WHERE event_name IN ('task.create', 'task.move.column')
+ GROUP BY task_id
+ ) src
+ WHERE id = src.task_id
+ )
+ WHERE (date_moved IS NULL OR date_moved = 0) AND id IN (
+ SELECT task_id
+ FROM (
+ SELECT task_id, max(date_creation) md
+ FROM project_activities
+ WHERE event_name IN ('task.create', 'task.move.column')
+ GROUP BY task_id
+ ) src
+ )");
+
+ // If there is no activities for some tasks use the date_creation
+ $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0");
+}
+
+function version_44($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN disable_login_form TINYINT(1) DEFAULT 0');
+}
+
+function version_43($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('subtask_restriction', '0'));
+ $rq->execute(array('subtask_time_tracking', '0'));
+
+ $pdo->exec("
+ CREATE TABLE subtask_time_tracking (
+ id INT NOT NULL AUTO_INCREMENT,
+ user_id INT NOT NULL,
+ subtask_id INT NOT NULL,
+ start INT DEFAULT 0,
+ end INT DEFAULT 0,
+ PRIMARY KEY(id),
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
+
+function version_42($pdo)
+{
+ $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT');
+}
+
+function version_41($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN timezone VARCHAR(50)');
+ $pdo->exec('ALTER TABLE users ADD COLUMN language CHAR(5)');
+}
+
+function version_40($pdo)
+{
+ // Avoid some full table scans
+ $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)');
+ $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)');
+ $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)');
+ $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)');
+ $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)');
+ $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)');
+ $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)');
+ $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)');
+
+ // Set the ownership for all private projects
+ $rq = $pdo->prepare('SELECT id FROM projects WHERE is_private=1');
+ $rq->execute();
+ $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0);
+
+ $rq = $pdo->prepare('UPDATE project_has_users SET is_owner=1 WHERE project_id=?');
+
+ foreach ($project_ids as $project_id) {
+ $rq->execute(array($project_id));
+ }
+}
+
+function version_39($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('project_categories', ''));
+}
+
+function version_38($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE swimlanes (
+ id INT NOT NULL AUTO_INCREMENT,
+ name VARCHAR(200) NOT NULL,
+ position INT DEFAULT 1,
+ is_active INT DEFAULT 1,
+ project_id INT,
+ PRIMARY KEY(id),
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE (name, project_id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INT DEFAULT 0');
+ $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INT DEFAULT 1");
+}
+
+function version_37($pdo)
+{
+ $pdo->exec("ALTER TABLE project_has_users ADD COLUMN is_owner TINYINT(1) DEFAULT '0'");
+}
+
+function version_36($pdo)
+{
+ $pdo->exec('ALTER TABLE tasks MODIFY title VARCHAR(255) NOT NULL');
+}
+
+function version_35($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_daily_summaries (
+ id INT NOT NULL AUTO_INCREMENT,
+ day CHAR(10) NOT NULL,
+ project_id INT NOT NULL,
+ column_id INT NOT NULL,
+ total INT NOT NULL DEFAULT 0,
+ PRIMARY KEY(id),
+ FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+
+ $pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
+}
function version_34($pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index f301f3e8..65a9c9bf 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -4,8 +4,469 @@ namespace Schema;
use PDO;
use Core\Security;
+use Model\Link;
-const VERSION = 15;
+const VERSION = 53;
+
+function version_53($pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN notifications_filter INTEGER DEFAULT 4");
+}
+
+function version_52($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('webhook_url', ''));
+
+ $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_creation'");
+ $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_modification'");
+}
+
+function version_51($pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN token VARCHAR(255) DEFAULT ''");
+}
+
+function version_50($pdo)
+{
+ $rq = $pdo->prepare("SELECT value FROM settings WHERE option='subtask_forecast'");
+ $rq->execute();
+ $result = $rq->fetch(PDO::FETCH_ASSOC);
+
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0));
+ $rq->execute(array('calendar_user_subtasks_time_tracking', 0));
+ $rq->execute(array('calendar_user_tasks', 'date_started'));
+ $rq->execute(array('calendar_project_tasks', 'date_started'));
+
+ $pdo->exec("DELETE FROM settings WHERE option='subtask_forecast'");
+}
+
+function version_49($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_jabber', '0'));
+ $rq->execute(array('integration_jabber_server', ''));
+ $rq->execute(array('integration_jabber_domain', ''));
+ $rq->execute(array('integration_jabber_username', ''));
+ $rq->execute(array('integration_jabber_password', ''));
+ $rq->execute(array('integration_jabber_nickname', 'kanboard'));
+ $rq->execute(array('integration_jabber_room', ''));
+
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber INTEGER DEFAULT '0'");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_server VARCHAR(255) DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_domain VARCHAR(255) DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_username VARCHAR(255) DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_password VARCHAR(255) DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_nickname VARCHAR(255) DEFAULT 'kanboard'");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_room VARCHAR(255) DEFAULT ''");
+}
+
+function version_48($pdo)
+{
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER');
+}
+
+function version_47($pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN identifier VARCHAR(50) DEFAULT ''");
+}
+
+function version_46($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_integrations (
+ id SERIAL PRIMARY KEY,
+ project_id INTEGER NOT NULL UNIQUE,
+ hipchat BOOLEAN DEFAULT '0',
+ hipchat_api_url VARCHAR(255) DEFAULT 'https://api.hipchat.com',
+ hipchat_room_id VARCHAR(255),
+ hipchat_room_token VARCHAR(255),
+ slack BOOLEAN DEFAULT '0',
+ slack_webhook_url VARCHAR(255),
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ )
+ ");
+}
+
+function version_45($pdo)
+{
+ $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INTEGER NOT NULL DEFAULT 0');
+}
+
+function version_44($pdo)
+{
+ $pdo->exec('ALTER TABLE project_has_categories ADD COLUMN description TEXT');
+}
+
+function version_43($pdo)
+{
+ $pdo->exec('ALTER TABLE files ADD COLUMN "date" INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE files ADD COLUMN "user_id" INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE files ADD COLUMN "size" INTEGER NOT NULL DEFAULT 0');
+}
+
+function version_42($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated BOOLEAN DEFAULT \'0\'');
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret CHAR(16)');
+}
+
+function version_41($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_gravatar', '0'));
+}
+
+function version_40($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_hipchat', '0'));
+ $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com'));
+ $rq->execute(array('integration_hipchat_room_id', ''));
+ $rq->execute(array('integration_hipchat_room_token', ''));
+}
+
+function version_39($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_slack_webhook', '0'));
+ $rq->execute(array('integration_slack_webhook_url', ''));
+}
+
+function version_38($pdo)
+{
+ $pdo->exec('CREATE TABLE currencies ("currency" CHAR(3) NOT NULL UNIQUE, "rate" REAL DEFAULT 0)');
+
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('application_currency', 'USD'));
+}
+
+function version_37($pdo)
+{
+ $pdo->exec('CREATE TABLE transitions (
+ "id" SERIAL PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "project_id" INTEGER NOT NULL,
+ "task_id" INTEGER NOT NULL,
+ "src_column_id" INTEGER NOT NULL,
+ "dst_column_id" INTEGER NOT NULL,
+ "date" INTEGER NOT NULL,
+ "time_spent" INTEGER DEFAULT 0,
+ FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)");
+ $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)");
+ $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)");
+}
+
+function version_36($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('subtask_forecast', '0'));
+}
+
+function version_35($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('application_stylesheet', ''));
+}
+
+function version_34($pdo)
+{
+ $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent REAL DEFAULT 0");
+}
+
+function version_33($pdo)
+{
+ $pdo->exec('CREATE TABLE budget_lines (
+ "id" SERIAL PRIMARY KEY,
+ "project_id" INTEGER NOT NULL,
+ "amount" REAL NOT NULL,
+ "date" VARCHAR(10) NOT NULL,
+ "comment" TEXT,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ )');
+}
+
+function version_32($pdo)
+{
+ $pdo->exec('CREATE TABLE timetable_day (
+ "id" SERIAL PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "start" VARCHAR(5) NOT NULL,
+ "end" VARCHAR(5) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE timetable_week (
+ "id" SERIAL PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "day" INTEGER NOT NULL,
+ "start" VARCHAR(5) NOT NULL,
+ "end" VARCHAR(5) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE timetable_off (
+ "id" SERIAL PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "date" VARCHAR(10) NOT NULL,
+ "all_day" BOOLEAN DEFAULT \'0\',
+ "start" VARCHAR(5) DEFAULT 0,
+ "end" VARCHAR(5) DEFAULT 0,
+ "comment" TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE timetable_extra (
+ "id" SERIAL PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "date" VARCHAR(10) NOT NULL,
+ "all_day" BOOLEAN DEFAULT \'0\',
+ "start" VARCHAR(5) DEFAULT 0,
+ "end" VARCHAR(5) DEFAULT 0,
+ "comment" TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+}
+
+function version_31($pdo)
+{
+ $pdo->exec("CREATE TABLE hourly_rates (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ rate REAL DEFAULT 0,
+ date_effective INTEGER NOT NULL,
+ currency CHAR(3) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )");
+}
+
+function version_30($pdo)
+{
+ $pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1');
+
+ $task_id = 0;
+ $position = 1;
+ $urq = $pdo->prepare('UPDATE subtasks SET position=? WHERE id=?');
+
+ $rq = $pdo->prepare('SELECT * FROM subtasks ORDER BY task_id, id ASC');
+ $rq->execute();
+
+ foreach ($rq->fetchAll(PDO::FETCH_ASSOC) as $subtask) {
+
+ if ($task_id != $subtask['task_id']) {
+ $position = 1;
+ $task_id = $subtask['task_id'];
+ }
+
+ $urq->execute(array($position, $subtask['id']));
+ $position++;
+ }
+}
+
+function version_29($pdo)
+{
+ $pdo->exec('ALTER TABLE task_has_files RENAME TO files');
+ $pdo->exec('ALTER TABLE task_has_subtasks RENAME TO subtasks');
+}
+
+function version_28($pdo)
+{
+ $pdo->exec('ALTER TABLE projects ADD COLUMN description TEXT');
+}
+
+function version_27($pdo)
+{
+ $pdo->exec('CREATE TABLE links (
+ "id" SERIAL PRIMARY KEY,
+ "label" VARCHAR(255) NOT NULL,
+ "opposite_id" INTEGER DEFAULT 0,
+ UNIQUE("label")
+ )');
+
+ $pdo->exec("CREATE TABLE task_has_links (
+ id SERIAL PRIMARY KEY,
+ link_id INTEGER NOT NULL,
+ task_id INTEGER NOT NULL,
+ opposite_task_id INTEGER NOT NULL,
+ FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )");
+
+ $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)");
+ $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)");
+
+ $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)');
+ $rq->execute(array('relates to', 0));
+ $rq->execute(array('blocks', 3));
+ $rq->execute(array('is blocked by', 2));
+ $rq->execute(array('duplicates', 5));
+ $rq->execute(array('is duplicated by', 4));
+ $rq->execute(array('is a child of', 7));
+ $rq->execute(array('is a parent of', 6));
+ $rq->execute(array('targets milestone', 9));
+ $rq->execute(array('is a milestone of', 8));
+ $rq->execute(array('fixes', 11));
+ $rq->execute(array('is fixed by', 10));
+}
+
+function version_26($pdo)
+{
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INT DEFAULT 0');
+
+ /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0.
+ * We take max project_activities.date_creation where event_name in task.create','task.move.column
+ * since creation date is always less than task moves
+ */
+ $pdo->exec("UPDATE tasks
+ SET date_moved = (
+ SELECT md
+ FROM (
+ SELECT task_id, max(date_creation) md
+ FROM project_activities
+ WHERE event_name IN ('task.create', 'task.move.column')
+ GROUP BY task_id
+ ) src
+ WHERE id = src.task_id
+ )
+ WHERE (date_moved IS NULL OR date_moved = 0) AND id IN (
+ SELECT task_id
+ FROM (
+ SELECT task_id, max(date_creation) md
+ FROM project_activities
+ WHERE event_name IN ('task.create', 'task.move.column')
+ GROUP BY task_id
+ ) src
+ )");
+
+ // If there is no activities for some tasks use the date_creation
+ $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0");
+}
+
+function version_25($pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN disable_login_form BOOLEAN DEFAULT '0'");
+}
+
+function version_24($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('subtask_restriction', '0'));
+ $rq->execute(array('subtask_time_tracking', '0'));
+
+ $pdo->exec('
+ CREATE TABLE subtask_time_tracking (
+ id SERIAL PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "subtask_id" INTEGER NOT NULL,
+ "start" INTEGER DEFAULT 0,
+ "end" INTEGER DEFAULT 0,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE
+ )
+ ');
+}
+
+function version_23($pdo)
+{
+ $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT');
+}
+
+function version_22($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN timezone VARCHAR(50)');
+ $pdo->exec('ALTER TABLE users ADD COLUMN language CHAR(5)');
+}
+
+function version_21($pdo)
+{
+ // Avoid some full table scans
+ $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)');
+ $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)');
+ $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)');
+ $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)');
+ $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)');
+ $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)');
+ $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)');
+ $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)');
+
+ // Set the ownership for all private projects
+ $rq = $pdo->prepare("SELECT id FROM projects WHERE is_private='1'");
+ $rq->execute();
+ $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0);
+
+ $rq = $pdo->prepare("UPDATE project_has_users SET is_owner='1' WHERE project_id=?");
+
+ foreach ($project_ids as $project_id) {
+ $rq->execute(array($project_id));
+ }
+}
+
+function version_20($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('project_categories', ''));
+}
+
+function version_19($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE swimlanes (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(200) NOT NULL,
+ position INTEGER DEFAULT 1,
+ is_active BOOLEAN DEFAULT '1',
+ project_id INTEGER,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE (name, project_id)
+ )
+ ");
+
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0');
+ $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane BOOLEAN DEFAULT '1'");
+}
+
+function version_18($pdo)
+{
+ $pdo->exec("ALTER TABLE project_has_users ADD COLUMN is_owner BOOLEAN DEFAULT '0'");
+}
+
+function version_17($pdo)
+{
+ $pdo->exec('ALTER TABLE tasks ALTER COLUMN title SET NOT NULL');
+}
+
+function version_16($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_daily_summaries (
+ id SERIAL PRIMARY KEY,
+ day CHAR(10) NOT NULL,
+ project_id INTEGER NOT NULL,
+ column_id INTEGER NOT NULL,
+ total INTEGER NOT NULL DEFAULT 0,
+ FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ )
+ ");
+
+ $pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
+}
function version_15($pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 8571d924..ceb3028c 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -4,8 +4,466 @@ namespace Schema;
use Core\Security;
use PDO;
+use Model\Link;
-const VERSION = 34;
+const VERSION = 71;
+
+function version_71($pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN notifications_filter INTEGER DEFAULT 4");
+}
+
+function version_70($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('webhook_url', ''));
+
+ $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_creation'");
+ $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_modification'");
+}
+
+function version_69($pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN token TEXT DEFAULT ''");
+}
+
+function version_68($pdo)
+{
+ $rq = $pdo->prepare("SELECT value FROM settings WHERE option='subtask_forecast'");
+ $rq->execute();
+ $result = $rq->fetch(PDO::FETCH_ASSOC);
+
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0));
+ $rq->execute(array('calendar_user_subtasks_time_tracking', 0));
+ $rq->execute(array('calendar_user_tasks', 'date_started'));
+ $rq->execute(array('calendar_project_tasks', 'date_started'));
+
+ $pdo->exec("DELETE FROM settings WHERE option='subtask_forecast'");
+}
+
+function version_67($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_jabber', '0'));
+ $rq->execute(array('integration_jabber_server', ''));
+ $rq->execute(array('integration_jabber_domain', ''));
+ $rq->execute(array('integration_jabber_username', ''));
+ $rq->execute(array('integration_jabber_password', ''));
+ $rq->execute(array('integration_jabber_nickname', 'kanboard'));
+ $rq->execute(array('integration_jabber_room', ''));
+
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber INTEGER DEFAULT '0'");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_server TEXT DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_domain TEXT DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_username TEXT DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_password TEXT DEFAULT ''");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_nickname TEXT DEFAULT 'kanboard'");
+ $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_room TEXT DEFAULT ''");
+}
+
+function version_66($pdo)
+{
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER');
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER');
+}
+
+function version_65($pdo)
+{
+ $pdo->exec("ALTER TABLE projects ADD COLUMN identifier TEXT DEFAULT ''");
+}
+
+function version_64($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_integrations (
+ id INTEGER PRIMARY KEY,
+ project_id INTEGER NOT NULL UNIQUE,
+ hipchat INTEGER DEFAULT 0,
+ hipchat_api_url TEXT DEFAULT 'https://api.hipchat.com',
+ hipchat_room_id TEXT,
+ hipchat_room_token TEXT,
+ slack INTEGER DEFAULT 0,
+ slack_webhook_url TEXT,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ )
+ ");
+}
+
+function version_63($pdo)
+{
+ $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INTEGER NOT NULL DEFAULT 0');
+}
+
+function version_62($pdo)
+{
+ $pdo->exec('ALTER TABLE project_has_categories ADD COLUMN description TEXT');
+}
+
+function version_61($pdo)
+{
+ $pdo->exec('ALTER TABLE files ADD COLUMN "date" INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE files ADD COLUMN "user_id" INTEGER NOT NULL DEFAULT 0');
+ $pdo->exec('ALTER TABLE files ADD COLUMN "size" INTEGER NOT NULL DEFAULT 0');
+}
+
+function version_60($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated INTEGER DEFAULT 0');
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret TEXT');
+}
+
+function version_59($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_gravatar', '0'));
+}
+
+function version_58($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_hipchat', '0'));
+ $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com'));
+ $rq->execute(array('integration_hipchat_room_id', ''));
+ $rq->execute(array('integration_hipchat_room_token', ''));
+}
+
+function version_57($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('integration_slack_webhook', '0'));
+ $rq->execute(array('integration_slack_webhook_url', ''));
+}
+
+function version_56($pdo)
+{
+ $pdo->exec('CREATE TABLE currencies ("currency" TEXT NOT NULL UNIQUE, "rate" REAL DEFAULT 0)');
+
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('application_currency', 'USD'));
+}
+
+function version_55($pdo)
+{
+ $pdo->exec('CREATE TABLE transitions (
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "project_id" INTEGER NOT NULL,
+ "task_id" INTEGER NOT NULL,
+ "src_column_id" INTEGER NOT NULL,
+ "dst_column_id" INTEGER NOT NULL,
+ "date" INTEGER NOT NULL,
+ "time_spent" INTEGER DEFAULT 0,
+ FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)");
+ $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)");
+ $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)");
+}
+
+function version_54($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('subtask_forecast', '0'));
+}
+
+function version_53($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('application_stylesheet', ''));
+}
+
+function version_52($pdo)
+{
+ $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent REAL DEFAULT 0");
+}
+
+function version_51($pdo)
+{
+ $pdo->exec('CREATE TABLE budget_lines (
+ "id" INTEGER PRIMARY KEY,
+ "project_id" INTEGER NOT NULL,
+ "amount" REAL NOT NULL,
+ "date" TEXT NOT NULL,
+ "comment" TEXT,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ )');
+}
+
+function version_50($pdo)
+{
+ $pdo->exec('CREATE TABLE timetable_day (
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "start" TEXT NOT NULL,
+ "end" TEXT NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE timetable_week (
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "day" INTEGER NOT NULL,
+ "start" TEXT NOT NULL,
+ "end" TEXT NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE timetable_off (
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "date" TEXT NOT NULL,
+ "all_day" INTEGER DEFAULT 0,
+ "start" TEXT DEFAULT 0,
+ "end" TEXT DEFAULT 0,
+ "comment" TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE timetable_extra (
+ "id" INTEGER PRIMARY KEY,
+ "user_id" INTEGER NOT NULL,
+ "date" TEXT NOT NULL,
+ "all_day" INTEGER DEFAULT 0,
+ "start" TEXT DEFAULT 0,
+ "end" TEXT DEFAULT 0,
+ "comment" TEXT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+}
+
+function version_49($pdo)
+{
+ $pdo->exec("CREATE TABLE hourly_rates (
+ id INTEGER PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ rate REAL DEFAULT 0,
+ date_effective INTEGER NOT NULL,
+ currency TEXT NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )");
+}
+
+function version_48($pdo)
+{
+ $pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1');
+
+ // Migrate all subtasks position
+
+ $task_id = 0;
+ $position = 1;
+ $urq = $pdo->prepare('UPDATE subtasks SET position=? WHERE id=?');
+
+ $rq = $pdo->prepare('SELECT * FROM subtasks ORDER BY task_id, id ASC');
+ $rq->execute();
+
+ foreach ($rq->fetchAll(PDO::FETCH_ASSOC) as $subtask) {
+
+ if ($task_id != $subtask['task_id']) {
+ $position = 1;
+ $task_id = $subtask['task_id'];
+ }
+
+ $urq->execute(array($position, $subtask['id']));
+ $position++;
+ }
+}
+
+function version_47($pdo)
+{
+ $pdo->exec('ALTER TABLE task_has_files RENAME TO files');
+ $pdo->exec('ALTER TABLE task_has_subtasks RENAME TO subtasks');
+}
+
+function version_46($pdo)
+{
+ $pdo->exec('ALTER TABLE projects ADD COLUMN description TEXT');
+}
+
+function version_45($pdo)
+{
+ $pdo->exec("CREATE TABLE links (
+ id INTEGER PRIMARY KEY,
+ label TEXT NOT NULL,
+ opposite_id INTEGER DEFAULT 0,
+ UNIQUE(label)
+ )");
+
+ $pdo->exec("CREATE TABLE task_has_links (
+ id INTEGER PRIMARY KEY,
+ link_id INTEGER NOT NULL,
+ task_id INTEGER NOT NULL,
+ opposite_task_id INTEGER NOT NULL,
+ FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+ FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )");
+
+ $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)");
+ $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)");
+
+ $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)');
+ $rq->execute(array('relates to', 0));
+ $rq->execute(array('blocks', 3));
+ $rq->execute(array('is blocked by', 2));
+ $rq->execute(array('duplicates', 5));
+ $rq->execute(array('is duplicated by', 4));
+ $rq->execute(array('is a child of', 7));
+ $rq->execute(array('is a parent of', 6));
+ $rq->execute(array('targets milestone', 9));
+ $rq->execute(array('is a milestone of', 8));
+ $rq->execute(array('fixes', 11));
+ $rq->execute(array('is fixed by', 10));
+}
+
+function version_44($pdo)
+{
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INTEGER DEFAULT 0');
+
+ /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0.
+ * We take max project_activities.date_creation where event_name in task.create','task.move.column
+ * since creation date is always less than task moves
+ */
+ $pdo->exec("UPDATE tasks
+ SET date_moved = (
+ SELECT md
+ FROM (
+ SELECT task_id, max(date_creation) md
+ FROM project_activities
+ WHERE event_name IN ('task.create', 'task.move.column')
+ GROUP BY task_id
+ ) src
+ WHERE id = src.task_id
+ )
+ WHERE (date_moved IS NULL OR date_moved = 0) AND id IN (
+ SELECT task_id
+ FROM (
+ SELECT task_id, max(date_creation) md
+ FROM project_activities
+ WHERE event_name IN ('task.create', 'task.move.column')
+ GROUP BY task_id
+ ) src
+ )");
+
+ // If there is no activities for some tasks use the date_creation
+ $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0");
+}
+
+function version_43($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN disable_login_form INTEGER DEFAULT 0');
+}
+
+function version_42($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('subtask_restriction', '0'));
+ $rq->execute(array('subtask_time_tracking', '0'));
+
+ $pdo->exec("
+ CREATE TABLE subtask_time_tracking (
+ id INTEGER PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ subtask_id INTEGER NOT NULL,
+ start INTEGER DEFAULT 0,
+ end INTEGER DEFAULT 0,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE
+ )
+ ");
+}
+
+function version_41($pdo)
+{
+ $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT');
+}
+
+function version_40($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN timezone TEXT');
+ $pdo->exec('ALTER TABLE users ADD COLUMN language TEXT');
+}
+
+function version_39($pdo)
+{
+ // Avoid some full table scans
+ $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)');
+ $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)');
+ $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)');
+ $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)');
+ $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)');
+ $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)');
+ $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)');
+ $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)');
+
+ // Set the ownership for all private projects
+ $rq = $pdo->prepare('SELECT id FROM projects WHERE is_private=1');
+ $rq->execute();
+ $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0);
+
+ $rq = $pdo->prepare('UPDATE project_has_users SET is_owner=1 WHERE project_id=?');
+
+ foreach ($project_ids as $project_id) {
+ $rq->execute(array($project_id));
+ }
+}
+
+function version_38($pdo)
+{
+ $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
+ $rq->execute(array('project_categories', ''));
+}
+
+function version_37($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE swimlanes (
+ id INTEGER PRIMARY KEY,
+ name TEXT,
+ position INTEGER DEFAULT 1,
+ is_active INTEGER DEFAULT 1,
+ project_id INTEGER,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ UNIQUE (name, project_id)
+ )
+ ");
+
+ $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0');
+ $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane TEXT DEFAULT 'Default swimlane'");
+ $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INTEGER DEFAULT 1");
+}
+
+function version_36($pdo)
+{
+ $pdo->exec('ALTER TABLE project_has_users ADD COLUMN is_owner INTEGER DEFAULT "0"');
+}
+
+function version_35($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE project_daily_summaries (
+ id INTEGER PRIMARY KEY,
+ day TEXT NOT NULL,
+ project_id INTEGER NOT NULL,
+ column_id INTEGER NOT NULL,
+ total INTEGER NOT NULL DEFAULT 0,
+ FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
+ )
+ ");
+
+ $pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
+}
function version_34($pdo)
{
@@ -440,7 +898,7 @@ function version_1($pdo)
$pdo->exec("
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
- title TEXT,
+ title TEXT NOCASE NOT NULL,
description TEXT,
date_creation INTEGER,
color_id TEXT,
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
new file mode 100644
index 00000000..28884b5a
--- /dev/null
+++ b/app/ServiceProvider/ClassProvider.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace ServiceProvider;
+
+use Core\Paginator;
+use Model\Config;
+use Model\Project;
+use Model\Webhook;
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+class ClassProvider implements ServiceProviderInterface
+{
+ private $classes = array(
+ 'Model' => array(
+ 'Acl',
+ 'Action',
+ 'Authentication',
+ 'Board',
+ 'Budget',
+ 'Category',
+ 'Color',
+ 'Comment',
+ 'Config',
+ 'Currency',
+ 'DateParser',
+ 'File',
+ 'HourlyRate',
+ 'LastLogin',
+ 'Link',
+ 'Notification',
+ 'Project',
+ 'ProjectActivity',
+ 'ProjectAnalytic',
+ 'ProjectDuplication',
+ 'ProjectDailySummary',
+ 'ProjectIntegration',
+ 'ProjectPermission',
+ 'Subtask',
+ 'SubtaskExport',
+ 'SubtaskForecast',
+ 'SubtaskTimeTracking',
+ 'Swimlane',
+ 'Task',
+ 'TaskCreation',
+ 'TaskDuplication',
+ 'TaskExport',
+ 'TaskFinder',
+ 'TaskFilter',
+ 'TaskLink',
+ 'TaskModification',
+ 'TaskPermission',
+ 'TaskPosition',
+ 'TaskStatus',
+ 'TaskValidator',
+ 'Timetable',
+ 'TimetableDay',
+ 'TimetableExtra',
+ 'TimetableWeek',
+ 'TimetableOff',
+ 'Transition',
+ 'User',
+ 'UserSession',
+ 'Webhook',
+ ),
+ 'Core' => array(
+ 'EmailClient',
+ 'Helper',
+ 'HttpClient',
+ 'MemoryCache',
+ 'Request',
+ 'Session',
+ 'Template',
+ ),
+ 'Integration' => array(
+ 'BitbucketWebhook',
+ 'GithubWebhook',
+ 'GitlabWebhook',
+ 'HipchatWebhook',
+ 'Jabber',
+ 'Mailgun',
+ 'Postmark',
+ 'SendgridWebhook',
+ 'SlackWebhook',
+ 'Smtp',
+ )
+ );
+
+ public function register(Container $container)
+ {
+ foreach ($this->classes as $namespace => $classes) {
+
+ foreach ($classes as $name) {
+
+ $class = '\\'.$namespace.'\\'.$name;
+
+ $container[lcfirst($name)] = function ($c) use ($class) {
+ return new $class($c);
+ };
+ }
+ }
+
+ $container['paginator'] = $container->factory(function ($c) {
+ return new Paginator($c);
+ });
+ }
+}
diff --git a/app/ServiceProvider/DatabaseProvider.php b/app/ServiceProvider/DatabaseProvider.php
new file mode 100644
index 00000000..e6a75a4e
--- /dev/null
+++ b/app/ServiceProvider/DatabaseProvider.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use PicoDb\Database;
+
+class DatabaseProvider implements ServiceProviderInterface
+{
+ public function register(Container $container)
+ {
+ $container['db'] = $this->getInstance();
+ $container['db']->stopwatch = DEBUG;
+ $container['db']->log_queries = DEBUG;
+ }
+
+ /**
+ * Setup the database driver and execute schema migration
+ *
+ * @access public
+ * @return \PicoDb\Database
+ */
+ public function getInstance()
+ {
+ switch (DB_DRIVER) {
+ case 'sqlite':
+ $db = $this->getSqliteInstance();
+ break;
+
+ case 'mysql':
+ $db = $this->getMysqlInstance();
+ break;
+
+ case 'postgres':
+ $db = $this->getPostgresInstance();
+ break;
+
+ default:
+ die('Database driver not supported');
+ }
+
+ if ($db->schema()->check(\Schema\VERSION)) {
+ return $db;
+ }
+ else {
+ $errors = $db->getLogMessages();
+ die('Unable to migrate database schema: <br/><br/><strong>'.(isset($errors[0]) ? $errors[0] : 'Unknown error').'</strong>');
+ }
+ }
+
+ /**
+ * Setup the Sqlite database driver
+ *
+ * @access private
+ * @return \PicoDb\Database
+ */
+ private function getSqliteInstance()
+ {
+ require_once __DIR__.'/../Schema/Sqlite.php';
+
+ return new Database(array(
+ 'driver' => 'sqlite',
+ 'filename' => DB_FILENAME
+ ));
+ }
+
+ /**
+ * Setup the Mysql database driver
+ *
+ * @access private
+ * @return \PicoDb\Database
+ */
+ private function getMysqlInstance()
+ {
+ require_once __DIR__.'/../Schema/Mysql.php';
+
+ return new Database(array(
+ 'driver' => 'mysql',
+ 'hostname' => DB_HOSTNAME,
+ 'username' => DB_USERNAME,
+ 'password' => DB_PASSWORD,
+ 'database' => DB_NAME,
+ 'charset' => 'utf8',
+ 'port' => DB_PORT,
+ ));
+ }
+
+ /**
+ * Setup the Postgres database driver
+ *
+ * @access private
+ * @return \PicoDb\Database
+ */
+ private function getPostgresInstance()
+ {
+ require_once __DIR__.'/../Schema/Postgres.php';
+
+ return new Database(array(
+ 'driver' => 'postgres',
+ 'hostname' => DB_HOSTNAME,
+ 'username' => DB_USERNAME,
+ 'password' => DB_PASSWORD,
+ 'database' => DB_NAME,
+ 'port' => DB_PORT,
+ ));
+ }
+}
diff --git a/app/ServiceProvider/EventDispatcherProvider.php b/app/ServiceProvider/EventDispatcherProvider.php
new file mode 100644
index 00000000..f566ede8
--- /dev/null
+++ b/app/ServiceProvider/EventDispatcherProvider.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace ServiceProvider;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Subscriber\AuthSubscriber;
+use Subscriber\BootstrapSubscriber;
+use Subscriber\NotificationSubscriber;
+use Subscriber\ProjectActivitySubscriber;
+use Subscriber\ProjectDailySummarySubscriber;
+use Subscriber\ProjectModificationDateSubscriber;
+use Subscriber\WebhookSubscriber;
+use Subscriber\SubtaskTimesheetSubscriber;
+use Subscriber\TaskMovedDateSubscriber;
+use Subscriber\TransitionSubscriber;
+use Subscriber\RecurringTaskSubscriber;
+
+class EventDispatcherProvider implements ServiceProviderInterface
+{
+ public function register(Container $container)
+ {
+ $container['dispatcher'] = new EventDispatcher;
+ $container['dispatcher']->addSubscriber(new BootstrapSubscriber($container));
+ $container['dispatcher']->addSubscriber(new AuthSubscriber($container));
+ $container['dispatcher']->addSubscriber(new ProjectActivitySubscriber($container));
+ $container['dispatcher']->addSubscriber(new ProjectDailySummarySubscriber($container));
+ $container['dispatcher']->addSubscriber(new ProjectModificationDateSubscriber($container));
+ $container['dispatcher']->addSubscriber(new WebhookSubscriber($container));
+ $container['dispatcher']->addSubscriber(new NotificationSubscriber($container));
+ $container['dispatcher']->addSubscriber(new SubtaskTimesheetSubscriber($container));
+ $container['dispatcher']->addSubscriber(new TaskMovedDateSubscriber($container));
+ $container['dispatcher']->addSubscriber(new TransitionSubscriber($container));
+ $container['dispatcher']->addSubscriber(new RecurringTaskSubscriber($container));
+
+ // Automatic actions
+ $container['action']->attachEvents();
+ }
+}
diff --git a/app/ServiceProvider/LoggingProvider.php b/app/ServiceProvider/LoggingProvider.php
new file mode 100644
index 00000000..dd79d654
--- /dev/null
+++ b/app/ServiceProvider/LoggingProvider.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace ServiceProvider;
+
+use Psr\Log\LogLevel;
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+use SimpleLogger\Logger;
+use SimpleLogger\Syslog;
+use SimpleLogger\File;
+
+class LoggingProvider implements ServiceProviderInterface
+{
+ public function register(Container $container)
+ {
+ $syslog = new Syslog('kanboard');
+ $syslog->setLevel(LogLevel::ERROR);
+
+ $logger = new Logger;
+ $logger->setLogger($syslog);
+
+ if (DEBUG) {
+ $logger->setLogger(new File(DEBUG_FILE));
+ }
+
+ $container['logger'] = $logger;
+ }
+}
diff --git a/app/Subscriber/AuthSubscriber.php b/app/Subscriber/AuthSubscriber.php
new file mode 100644
index 00000000..b814057f
--- /dev/null
+++ b/app/Subscriber/AuthSubscriber.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Subscriber;
+
+use Core\Request;
+use Event\AuthEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class AuthSubscriber extends \Core\Base implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ 'auth.success' => array('onSuccess', 0),
+ );
+ }
+
+ public function onSuccess(AuthEvent $event)
+ {
+ $this->lastLogin->create(
+ $event->getAuthType(),
+ $event->getUserId(),
+ Request::getIpAddress(),
+ Request::getUserAgent()
+ );
+ }
+}
diff --git a/app/Subscriber/BootstrapSubscriber.php b/app/Subscriber/BootstrapSubscriber.php
new file mode 100644
index 00000000..793ba3e7
--- /dev/null
+++ b/app/Subscriber/BootstrapSubscriber.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Subscriber;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class BootstrapSubscriber extends \Core\Base implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ 'session.bootstrap' => array('setup', 0),
+ 'api.bootstrap' => array('setup', 0),
+ 'console.bootstrap' => array('setup', 0),
+ );
+ }
+
+ public function setup()
+ {
+ $this->config->setupTranslations();
+ $this->config->setupTimezone();
+ }
+}
diff --git a/app/Subscriber/NotificationSubscriber.php b/app/Subscriber/NotificationSubscriber.php
new file mode 100644
index 00000000..41fd6aef
--- /dev/null
+++ b/app/Subscriber/NotificationSubscriber.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Subscriber;
+
+use Event\GenericEvent;
+use Model\Task;
+use Model\Comment;
+use Model\Subtask;
+use Model\File;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class NotificationSubscriber extends \Core\Base 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_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),
+ );
+ }
+
+ public function execute(GenericEvent $event, $event_name)
+ {
+ $this->notification->sendNotifications($event_name, $this->getEventData($event));
+ }
+
+ public function getEventData(GenericEvent $event)
+ {
+ $values = array();
+
+ switch (get_class($event)) {
+ case 'Event\TaskEvent':
+ $values['task'] = $this->taskFinder->getDetails($event['task_id']);
+ break;
+ case 'Event\SubtaskEvent':
+ $values['subtask'] = $this->subtask->getById($event['id'], true);
+ $values['task'] = $this->taskFinder->getDetails($values['subtask']['task_id']);
+ break;
+ case 'Event\FileEvent':
+ $values['file'] = $event->getAll();
+ $values['task'] = $this->taskFinder->getDetails($values['file']['task_id']);
+ break;
+ case 'Event\CommentEvent':
+ $values['comment'] = $this->comment->getById($event['id']);
+ $values['task'] = $this->taskFinder->getDetails($values['comment']['task_id']);
+ break;
+ }
+
+ return $values;
+ }
+}
diff --git a/app/Subscriber/ProjectActivitySubscriber.php b/app/Subscriber/ProjectActivitySubscriber.php
new file mode 100644
index 00000000..31f771f8
--- /dev/null
+++ b/app/Subscriber/ProjectActivitySubscriber.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Subscriber;
+
+use Event\GenericEvent;
+use Model\Task;
+use Model\Comment;
+use Model\Subtask;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class ProjectActivitySubscriber extends \Core\Base implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0),
+ Task::EVENT_UPDATE => array('execute', 0),
+ Task::EVENT_CREATE => 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),
+ Comment::EVENT_UPDATE => array('execute', 0),
+ Comment::EVENT_CREATE => array('execute', 0),
+ Subtask::EVENT_UPDATE => array('execute', 0),
+ Subtask::EVENT_CREATE => array('execute', 0),
+ );
+ }
+
+ public function execute(GenericEvent $event, $event_name)
+ {
+ // Executed only when someone is logged
+ if ($this->userSession->isLogged() && isset($event['task_id'])) {
+
+ $values = $this->getValues($event);
+
+ $this->projectActivity->createEvent(
+ $values['task']['project_id'],
+ $values['task']['id'],
+ $this->userSession->getId(),
+ $event_name,
+ $values
+ );
+
+ // Send notifications to third-party services
+ foreach (array('slackWebhook', 'hipchatWebhook', 'jabber') as $model) {
+ $this->$model->notify(
+ $values['task']['project_id'],
+ $values['task']['id'],
+ $event_name,
+ $values
+ );
+ }
+ }
+ }
+
+ private function getValues(GenericEvent $event)
+ {
+ $values = array();
+ $values['task'] = $this->taskFinder->getDetails($event['task_id']);
+
+ switch (get_class($event)) {
+ case 'Event\SubtaskEvent':
+ $values['subtask'] = $this->subtask->getById($event['id'], true);
+ break;
+ case 'Event\CommentEvent':
+ $values['comment'] = $this->comment->getById($event['id']);
+ break;
+ }
+
+ return $values;
+ }
+}
diff --git a/app/Subscriber/ProjectDailySummarySubscriber.php b/app/Subscriber/ProjectDailySummarySubscriber.php
new file mode 100644
index 00000000..9e4f15b0
--- /dev/null
+++ b/app/Subscriber/ProjectDailySummarySubscriber.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Subscriber;
+
+use Event\TaskEvent;
+use Model\Task;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class ProjectDailySummarySubscriber extends \Core\Base 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),
+ );
+ }
+
+ public function execute(TaskEvent $event)
+ {
+ if (isset($event['project_id'])) {
+ $this->projectDailySummary->updateTotals($event['project_id'], date('Y-m-d'));
+ }
+ }
+}
diff --git a/app/Subscriber/ProjectModificationDateSubscriber.php b/app/Subscriber/ProjectModificationDateSubscriber.php
new file mode 100644
index 00000000..2c01173b
--- /dev/null
+++ b/app/Subscriber/ProjectModificationDateSubscriber.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Subscriber;
+
+use Event\GenericEvent;
+use Model\Task;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class ProjectModificationDateSubscriber extends \Core\Base 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),
+ );
+ }
+
+ public function execute(GenericEvent $event)
+ {
+ if (isset($event['project_id'])) {
+ $this->project->updateModificationDate($event['project_id']);
+ }
+ }
+}
diff --git a/app/Subscriber/RecurringTaskSubscriber.php b/app/Subscriber/RecurringTaskSubscriber.php
new file mode 100644
index 00000000..68d704f0
--- /dev/null
+++ b/app/Subscriber/RecurringTaskSubscriber.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Subscriber;
+
+use Event\TaskEvent;
+use Model\Task;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class RecurringTaskSubscriber extends \Core\Base implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ Task::EVENT_MOVE_COLUMN => array('onMove', 0),
+ Task::EVENT_CLOSE => array('onClose', 0),
+ );
+ }
+
+ public function onMove(TaskEvent $event)
+ {
+ if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) {
+
+ if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->board->getFirstColumn($event['project_id']) == $event['src_column_id']) {
+ $this->taskDuplication->duplicateRecurringTask($event['task_id']);
+ }
+ else if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->board->getLastColumn($event['project_id']) == $event['dst_column_id']) {
+ $this->taskDuplication->duplicateRecurringTask($event['task_id']);
+ }
+ }
+ }
+
+ public function onClose(TaskEvent $event)
+ {
+ 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/SubtaskTimesheetSubscriber.php b/app/Subscriber/SubtaskTimesheetSubscriber.php
new file mode 100644
index 00000000..fdaf442f
--- /dev/null
+++ b/app/Subscriber/SubtaskTimesheetSubscriber.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Subscriber;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Model\Subtask;
+use Event\SubtaskEvent;
+
+class SubtaskTimesheetSubscriber extends \Core\Base implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ Subtask::EVENT_CREATE => array('updateTaskTime', 0),
+ Subtask::EVENT_UPDATE => array(
+ array('logStartEnd', 10),
+ array('updateTaskTime', 0),
+ )
+ );
+ }
+
+ public function updateTaskTime(SubtaskEvent $event)
+ {
+ if (isset($event['task_id'])) {
+ $this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']);
+ }
+ }
+
+ public function logStartEnd(SubtaskEvent $event)
+ {
+ if ($this->config->get('subtask_time_tracking') == 1 && isset($event['status'])) {
+
+ $subtask = $this->subtask->getById($event['id']);
+
+ if (empty($subtask['user_id'])) {
+ return false;
+ }
+
+ if ($subtask['status'] == Subtask::STATUS_INPROGRESS) {
+ return $this->subtaskTimeTracking->logStartTime($subtask['id'], $subtask['user_id']);
+ }
+ else {
+ return $this->subtaskTimeTracking->logEndTime($subtask['id'], $subtask['user_id']);
+ }
+ }
+ }
+}
diff --git a/app/Subscriber/TaskMovedDateSubscriber.php b/app/Subscriber/TaskMovedDateSubscriber.php
new file mode 100644
index 00000000..eb04d62c
--- /dev/null
+++ b/app/Subscriber/TaskMovedDateSubscriber.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Subscriber;
+
+use Event\TaskEvent;
+use Model\Task;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class TaskMovedDateSubscriber extends \Core\Base implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ Task::EVENT_MOVE_COLUMN => 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
new file mode 100644
index 00000000..5804dab7
--- /dev/null
+++ b/app/Subscriber/TransitionSubscriber.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Subscriber;
+
+use Event\TaskEvent;
+use Model\Task;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class TransitionSubscriber extends \Core\Base implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ Task::EVENT_MOVE_COLUMN => array('execute', 0),
+ );
+ }
+
+ public function execute(TaskEvent $event)
+ {
+ $user_id = $this->userSession->getId();
+
+ if (! empty($user_id)) {
+ $this->transition->save($user_id, $event->getAll());
+ }
+ }
+} \ No newline at end of file
diff --git a/app/Subscriber/WebhookSubscriber.php b/app/Subscriber/WebhookSubscriber.php
new file mode 100644
index 00000000..5176a7ff
--- /dev/null
+++ b/app/Subscriber/WebhookSubscriber.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Subscriber;
+
+use Event\CommentEvent;
+use Event\GenericEvent;
+use Event\TaskEvent;
+use Model\Comment;
+use Model\Task;
+use Model\File;
+use Model\Subtask;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class WebhookSubscriber extends \Core\Base 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_ASSIGNEE_CHANGE => array('execute', 0),
+ Task::EVENT_MOVE_PROJECT => array('execute', 0),
+ Task::EVENT_MOVE_SWIMLANE => array('execute', 0),
+ Comment::EVENT_CREATE => array('execute', 0),
+ Comment::EVENT_UPDATE => array('execute', 0),
+ File::EVENT_CREATE => array('execute', 0),
+ Subtask::EVENT_CREATE => array('execute', 0),
+ Subtask::EVENT_UPDATE => array('execute', 0),
+ );
+ }
+
+ public function execute(GenericEvent $event, $event_name)
+ {
+ $payload = array(
+ 'event_name' => $event_name,
+ 'event_data' => $event->getAll(),
+ );
+
+ $this->webhook->notify($payload);
+ }
+}
diff --git a/app/Template/action/event.php b/app/Template/action/event.php
new file mode 100644
index 00000000..7f968a97
--- /dev/null
+++ b/app/Template/action/event.php
@@ -0,0 +1,25 @@
+<div class="page-header">
+ <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
+</div>
+
+<h3><?= t('Choose an event') ?></h3>
+<form method="post" action="<?= $this->url->href('action', 'params', array('project_id' => $project['id'])) ?>">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('project_id', $values) ?>
+ <?= $this->form->hidden('action_name', $values) ?>
+
+ <?= $this->form->label(t('Event'), 'event_name') ?>
+ <?= $this->form->select('event_name', $events, $values) ?><br/>
+
+ <div class="form-help">
+ <?= t('When the selected event occurs execute the corresponding action.') ?>
+ </div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/action/index.php b/app/Template/action/index.php
new file mode 100644
index 00000000..9e98554c
--- /dev/null
+++ b/app/Template/action/index.php
@@ -0,0 +1,64 @@
+<div class="page-header">
+ <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
+</div>
+
+<?php if (! empty($actions)): ?>
+
+<h3><?= t('Defined actions') ?></h3>
+<table>
+ <tr>
+ <th><?= t('Event name') ?></th>
+ <th><?= t('Action name') ?></th>
+ <th><?= t('Action parameters') ?></th>
+ <th><?= t('Action') ?></th>
+ </tr>
+
+ <?php foreach ($actions as $action): ?>
+ <tr>
+ <td><?= $this->text->in($action['event_name'], $available_events) ?></td>
+ <td><?= $this->text->in($action['action_name'], $available_actions) ?></td>
+ <td>
+ <ul>
+ <?php foreach ($action['params'] as $param): ?>
+ <li>
+ <?= $this->text->in($param['name'], $available_params) ?> =
+ <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'], 'label')): ?>
+ <?= $this->e($param['value']) ?>
+ <?php endif ?>
+ </strong>
+ </li>
+ <?php endforeach ?>
+ </ul>
+ </td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'action', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<?php endif ?>
+
+<h3><?= t('Add an action') ?></h3>
+<form method="post" action="<?= $this->url->href('action', 'event', array('project_id' => $project['id'])) ?>" class="listing">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('project_id', $values) ?>
+
+ <?= $this->form->label(t('Action'), 'action_name') ?>
+ <?= $this->form->select('action_name', $available_actions, $values) ?><br/>
+
+ <div class="form-actions">
+ <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
new file mode 100644
index 00000000..685cbcc5
--- /dev/null
+++ b/app/Template/action/params.php
@@ -0,0 +1,43 @@
+<div class="page-header">
+ <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
+</div>
+
+<h3><?= t('Define action parameters') ?></h3>
+<form method="post" action="<?= $this->url->href('action', 'create', array('project_id' => $project['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('project_id', $values) ?>
+ <?= $this->form->hidden('event_name', $values) ?>
+ <?= $this->form->hidden('action_name', $values) ?>
+
+ <?php foreach ($action_params as $param_name => $param_desc): ?>
+
+ <?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/>
+ <?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/>
+ <?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/>
+ <?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/>
+ <?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/>
+ <?php elseif ($this->text->contains($param_name, 'label')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->text('params['.$param_name.']', $values) ?>
+ <?php endif ?>
+
+ <?php endforeach ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/action/remove.php b/app/Template/action/remove.php
new file mode 100644
index 00000000..c8d4dfe4
--- /dev/null
+++ b/app/Template/action/remove.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Remove an automatic action') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this action: "%s"?', $this->text->in($action['event_name'], $available_events).'/'.$this->text->in($action['action_name'], $available_actions)) ?>
+ </p>
+
+ <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'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/analytic/burndown.php b/app/Template/analytic/burndown.php
new file mode 100644
index 00000000..839573be
--- /dev/null
+++ b/app/Template/analytic/burndown.php
@@ -0,0 +1,34 @@
+<div class="page-header">
+ <h2><?= t('Burndown chart') ?></h2>
+</div>
+
+<?php if (! $display_graph): ?>
+ <p class="alert"><?= t('Not enough data to show the graph.') ?></p>
+<?php else: ?>
+ <section id="analytic-burndown">
+ <div id="chart" data-url="<?= $this->url->href('analytic', 'burndown', array('project_id' => $project['id'], 'from' => $values['from'], 'to' => $values['to'])) ?>"></div>
+ </section>
+<?php endif ?>
+
+<hr/>
+
+<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'burndown', array('project_id' => $project['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <div class="form-inline-group">
+ <?= $this->form->label(t('Start Date'), 'from') ?>
+ <?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
+ </div>
+
+ <div class="form-inline-group">
+ <?= $this->form->label(t('End Date'), 'to') ?>
+ <?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
+ </div>
+
+ <div class="form-inline-group">
+ <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<p class="alert alert-info"><?= t('This chart show the task complexity over the time (Work Remaining).') ?></p>
diff --git a/app/Template/analytic/cfd.php b/app/Template/analytic/cfd.php
new file mode 100644
index 00000000..26696b31
--- /dev/null
+++ b/app/Template/analytic/cfd.php
@@ -0,0 +1,32 @@
+<div class="page-header">
+ <h2><?= t('Cumulative flow diagram') ?></h2>
+</div>
+
+<?php if (! $display_graph): ?>
+ <p class="alert"><?= t('Not enough data to show the graph.') ?></p>
+<?php else: ?>
+ <section id="analytic-cfd">
+ <div id="chart" data-url="<?= $this->url->href('analytic', 'cfd', array('project_id' => $project['id'], 'from' => $values['from'], 'to' => $values['to'])) ?>"></div>
+ </section>
+<?php endif ?>
+
+<hr/>
+
+<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'cfd', array('project_id' => $project['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <div class="form-inline-group">
+ <?= $this->form->label(t('Start Date'), 'from') ?>
+ <?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
+ </div>
+
+ <div class="form-inline-group">
+ <?= $this->form->label(t('End Date'), 'to') ?>
+ <?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
+ </div>
+
+ <div class="form-inline-group">
+ <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
+ </div>
+</form>
diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php
new file mode 100644
index 00000000..de8d0de9
--- /dev/null
+++ b/app/Template/analytic/layout.php
@@ -0,0 +1,35 @@
+<?= $this->asset->js('assets/js/vendor/d3.v3.4.8.min.js') ?>
+<?= $this->asset->js('assets/js/vendor/dimple.v2.1.2.min.js') ?>
+
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <span class="dropdown">
+ <span>
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <ul>
+ <?= $this->render('project/dropdown', array('project' => $project)) ?>
+ </ul>
+ </span>
+ </span>
+ </li>
+ <li>
+ <i class="fa fa-table fa-fw"></i>
+ <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-folder fa-fw"></i>
+ <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ </li>
+ </ul>
+ </div>
+ <section class="sidebar-container" id="analytic-section">
+
+ <?= $this->render('analytic/sidebar', array('project' => $project)) ?>
+
+ <div class="sidebar-content">
+ <?= $content_for_sublayout ?>
+ </div>
+ </section>
+</section> \ No newline at end of file
diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php
new file mode 100644
index 00000000..2d1a7c96
--- /dev/null
+++ b/app/Template/analytic/sidebar.php
@@ -0,0 +1,17 @@
+<div class="sidebar">
+ <h2><?= t('Reportings') ?></h2>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Task distribution'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('User repartition'), 'analytic', 'users', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Burndown chart'), 'analytic', 'burndown', array('project_id' => $project['id'])) ?>
+ </li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/analytic/tasks.php b/app/Template/analytic/tasks.php
new file mode 100644
index 00000000..faa4bacc
--- /dev/null
+++ b/app/Template/analytic/tasks.php
@@ -0,0 +1,34 @@
+<div class="page-header">
+ <h2><?= t('Task distribution') ?></h2>
+</div>
+
+<?php if (empty($metrics)): ?>
+ <p class="alert"><?= t('Not enough data to show the graph.') ?></p>
+<?php else: ?>
+ <section id="analytic-task-repartition">
+
+ <div id="chart" data-url="<?= $this->url->href('analytic', 'tasks', array('project_id' => $project['id'])) ?>"></div>
+
+ <table>
+ <tr>
+ <th><?= t('Column') ?></th>
+ <th><?= t('Number of tasks') ?></th>
+ <th><?= t('Percentage') ?></th>
+ </tr>
+ <?php foreach ($metrics as $metric): ?>
+ <tr>
+ <td>
+ <?= $this->e($metric['column_title']) ?>
+ </td>
+ <td>
+ <?= $metric['nb_tasks'] ?>
+ </td>
+ <td>
+ <?= n($metric['percentage']) ?>%
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ </section>
+<?php endif ?>
diff --git a/app/Template/analytic/users.php b/app/Template/analytic/users.php
new file mode 100644
index 00000000..982ef206
--- /dev/null
+++ b/app/Template/analytic/users.php
@@ -0,0 +1,34 @@
+<div class="page-header">
+ <h2><?= t('User repartition') ?></h2>
+</div>
+
+<?php if (empty($metrics)): ?>
+ <p class="alert"><?= t('Not enough data to show the graph.') ?></p>
+<?php else: ?>
+ <section id="analytic-user-repartition">
+
+ <div id="chart" data-url="<?= $this->url->href('analytic', 'users', array('project_id' => $project['id'])) ?>"></div>
+
+ <table>
+ <tr>
+ <th><?= t('User') ?></th>
+ <th><?= t('Number of tasks') ?></th>
+ <th><?= t('Percentage') ?></th>
+ </tr>
+ <?php foreach ($metrics as $metric): ?>
+ <tr>
+ <td>
+ <?= $this->e($metric['user']) ?>
+ </td>
+ <td>
+ <?= $metric['nb_tasks'] ?>
+ </td>
+ <td>
+ <?= n($metric['percentage']) ?>%
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ </section>
+<?php endif ?>
diff --git a/app/Template/app/dashboard.php b/app/Template/app/dashboard.php
new file mode 100644
index 00000000..faf49ef5
--- /dev/null
+++ b/app/Template/app/dashboard.php
@@ -0,0 +1,60 @@
+<section id="main">
+ <div class="page-header page-header-mobile">
+ <ul>
+ <?php if ($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('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 ?>
+ <li>
+ <span class="dropdown">
+ <span>
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Change dashboard view') ?></a>
+ <ul>
+ <li>
+ <a href="#" class="dashboard-toggle" data-toggle="projects"><?= t('Show/hide projects') ?></a>
+ </li>
+ <li>
+ <a href="#" class="dashboard-toggle" data-toggle="tasks"><?= t('Show/hide tasks') ?></a>
+ </li>
+ <li>
+ <a href="#" class="dashboard-toggle" data-toggle="subtasks"><?= t('Show/hide subtasks') ?></a>
+ </li>
+ <li>
+ <a href="#" class="dashboard-toggle" data-toggle="calendar"><?= t('Show/hide calendar') ?></a>
+ </li>
+ <li>
+ <a href="#" class="dashboard-toggle" data-toggle="activities"><?= t('Show/hide activities') ?></a>
+ </li>
+ </ul>
+ </span>
+ </span>
+ </li>
+ </ul>
+ </div>
+ <section id="dashboard">
+ <div class="dashboard-left-column">
+ <div id="dashboard-projects"><?= $this->render('app/projects', array('paginator' => $project_paginator)) ?></div>
+ <div id="dashboard-tasks"><?= $this->render('app/tasks', array('paginator' => $task_paginator)) ?></div>
+ <div id="dashboard-subtasks"><?= $this->render('app/subtasks', array('paginator' => $subtask_paginator)) ?></div>
+ </div>
+ <div class="dashboard-right-column">
+ <div id="dashboard-calendar">
+ <div id="user-calendar"
+ data-check-url="<?= $this->url->href('calendar', 'user') ?>"
+ data-user-id="<?= $user_id ?>"
+ data-save-url="<?= $this->url->href('calendar', 'save') ?>"
+ >
+ </div>
+ </div>
+ <div id="dashboard-activities">
+ <h2><?= t('Activity stream') ?></h2>
+ <?= $this->render('event/events', array('events' => $events)) ?>
+ </div>
+ </div>
+ </section>
+</section> \ No newline at end of file
diff --git a/app/Templates/app_forbidden.php b/app/Template/app/forbidden.php
index 0c035404..96e76115 100644
--- a/app/Templates/app_forbidden.php
+++ b/app/Template/app/forbidden.php
@@ -1,8 +1,4 @@
<section id="main">
- <div class="page-header">
- <h2><?= t('Forbidden') ?></h2>
- </div>
-
<p class="alert alert-error">
<?= t('Access Forbidden') ?>
</p>
diff --git a/app/Template/app/notfound.php b/app/Template/app/notfound.php
new file mode 100644
index 00000000..0419902c
--- /dev/null
+++ b/app/Template/app/notfound.php
@@ -0,0 +1,5 @@
+<section id="main">
+ <p class="alert alert-error">
+ <?= t('Sorry, I didn\'t find this information in my database!') ?>
+ </p>
+</section> \ No newline at end of file
diff --git a/app/Template/app/projects.php b/app/Template/app/projects.php
new file mode 100644
index 00000000..90e6e67d
--- /dev/null
+++ b/app/Template/app/projects.php
@@ -0,0 +1,41 @@
+<h2><?= t('My projects') ?></h2>
+<?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('Your are not member of any project.') ?></p>
+<?php else: ?>
+ <table class="table-fixed">
+ <tr>
+ <th class="column-8"><?= $paginator->order('Id', 'id') ?></th>
+ <th class="column-20"><?= $paginator->order(t('Project'), 'name') ?></th>
+ <th><?= t('Columns') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $project): ?>
+ <tr>
+ <td>
+ <?= $this->url->link('#'.$project['id'], 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link') ?>
+ </td>
+ <td>
+ <?php if ($this->user->isManager($project['id'])): ?>
+ <?= $this->url->link('<i class="fa fa-cog"></i>', 'project', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Settings')) ?>&nbsp;
+ <?php endif ?>
+
+ <?= $this->url->link('<i class="fa fa-calendar"></i>', 'calendar', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Calendar')) ?>&nbsp;
+
+ <?= $this->url->link($this->e($project['name']), 'board', 'show', array('project_id' => $project['id'])) ?>
+ <?php if (! empty($project['description'])): ?>
+ <span class="column-tooltip" title='<?= $this->e($this->text->markdown($project['description'])) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?php endif ?>
+ </td>
+ <td class="dashboard-project-stats">
+ <?php foreach ($project['columns'] as $column): ?>
+ <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong>
+ <span><?= $this->e($column['title']) ?></span>
+ <?php endforeach ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+<?php endif ?>
diff --git a/app/Template/app/subtasks.php b/app/Template/app/subtasks.php
new file mode 100644
index 00000000..5afb71b0
--- /dev/null
+++ b/app/Template/app/subtasks.php
@@ -0,0 +1,41 @@
+<h2><?= t('My subtasks') ?></h2>
+<?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is nothing assigned to you.') ?></p>
+<?php else: ?>
+ <table class="table-fixed">
+ <tr>
+ <th class="column-10"><?= $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>
+ <th class="column-20"><?= t('Time tracking') ?></th>
+ </tr>
+ <?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'])) ?>
+ </td>
+ <td>
+ <?= $this->url->link($this->e($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?>
+ </td>
+ <td>
+ <?= $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') ?>
+ </td>
+ <td>
+ <?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 ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/app/tasks.php b/app/Template/app/tasks.php
new file mode 100644
index 00000000..f05c63ef
--- /dev/null
+++ b/app/Template/app/tasks.php
@@ -0,0 +1,41 @@
+<h2><?= t('My tasks') ?></h2>
+<?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is nothing assigned to you.') ?></p>
+<?php else: ?>
+ <table class="table-fixed">
+ <tr>
+ <th class="column-8"><?= $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>
+ <th class="column-20"><?= $paginator->order(t('Due date'), 'date_due') ?></th>
+ </tr>
+ <?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'])) ?>
+ </td>
+ <td>
+ <?= $this->url->link($this->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?>
+ </td>
+ <td>
+ <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </td>
+ <td>
+ <?php if (! empty($task['time_spent'])): ?>
+ <strong><?= $this->e($task['time_spent']).'h' ?></strong> <?= t('spent') ?>
+ <?php endif ?>
+
+ <?php if (! empty($task['time_estimated'])): ?>
+ <strong><?= $this->e($task['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <?= dt('%B %e, %Y', $task['date_due']) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/auth/index.php b/app/Template/auth/index.php
new file mode 100644
index 00000000..8801a512
--- /dev/null
+++ b/app/Template/auth/index.php
@@ -0,0 +1,32 @@
+<div class="form-login">
+
+ <?php if (isset($errors['login'])): ?>
+ <p class="alert alert-error"><?= $this->e($errors['login']) ?></p>
+ <?php endif ?>
+
+ <form method="post" action="<?= $this->url->href('auth', 'check', array('redirect_query' => $redirect_query)) ?>">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Username'), 'username') ?>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?><br/>
+
+ <?= $this->form->label(t('Password'), 'password') ?>
+ <?= $this->form->password('password', $values, $errors, array('required')) ?>
+
+ <?= $this->form->checkbox('remember_me', t('Remember Me'), 1) ?><br/>
+
+ <?php if (GOOGLE_AUTH): ?>
+ <?= $this->url->link(t('Login with my Google Account'), 'user', 'google') ?>
+ <?php endif ?>
+
+ <?php if (GITHUB_AUTH): ?>
+ <?= $this->url->link(t('Login with my GitHub Account'), 'user', 'gitHub') ?>
+ <?php endif ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
+ </div>
+ </form>
+
+</div> \ No newline at end of file
diff --git a/app/Template/board/assignee.php b/app/Template/board/assignee.php
new file mode 100644
index 00000000..4af19cf7
--- /dev/null
+++ b/app/Template/board/assignee.php
@@ -0,0 +1,21 @@
+<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'])) ?>">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $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/>
+
+ <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>
+</section> \ No newline at end of file
diff --git a/app/Template/board/category.php b/app/Template/board/category.php
new file mode 100644
index 00000000..b38758d3
--- /dev/null
+++ b/app/Template/board/category.php
@@ -0,0 +1,22 @@
+<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'])) ?>">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $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) ?><br/>
+
+ <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>
+
+</section> \ No newline at end of file
diff --git a/app/Template/board/comments.php b/app/Template/board/comments.php
new file mode 100644
index 00000000..75816af6
--- /dev/null
+++ b/app/Template/board/comments.php
@@ -0,0 +1,13 @@
+<section>
+ <?php foreach ($comments as $comment): ?>
+ <p class="comment-title">
+ <span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @ <span class="comment-date"><?= dt('%b %e, %Y, %k:%M %p', $comment['date']) ?></span>
+ </p>
+
+ <div class="comment-inner">
+ <div class="markdown">
+ <?= $this->text->markdown($comment['comment']) ?>
+ </div>
+ </div>
+ <?php endforeach ?>
+</section>
diff --git a/app/Template/board/description.php b/app/Template/board/description.php
new file mode 100644
index 00000000..7e0e3430
--- /dev/null
+++ b/app/Template/board/description.php
@@ -0,0 +1,5 @@
+<section class="tooltip-large">
+<div class="markdown">
+ <?= $this->text->markdown($task['description']) ?>
+</div>
+</section> \ No newline at end of file
diff --git a/app/Template/board/files.php b/app/Template/board/files.php
new file mode 100644
index 00000000..81136659
--- /dev/null
+++ b/app/Template/board/files.php
@@ -0,0 +1,31 @@
+<section>
+ <table>
+ <?php if (! empty($images)): ?>
+ <?php foreach ($images as $file): ?>
+ <tr>
+ <td class="column-70">
+ <i class="fa fa-file-image-o fa-fw"></i>
+ <?= $this->e($file['name']) ?>
+ </td>
+ <td>
+ <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'])) ?>
+ <i class="fa fa-eye"></i> <?= $this->url->link(t('open'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ <?php endif ?>
+ <?php if (! empty($files)): ?>
+ <?php foreach ($files as $file): ?>
+ <tr>
+ <td>
+ <i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i>
+ <?= $this->e($file['name']) ?>
+ </td>
+ <td>
+ <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'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ <?php endif ?>
+ </table>
+</section>
diff --git a/app/Template/board/filters.php b/app/Template/board/filters.php
new file mode 100644
index 00000000..bf2adfac
--- /dev/null
+++ b/app/Template/board/filters.php
@@ -0,0 +1,43 @@
+<div class="page-header">
+ <ul class="board-filters">
+ <li>
+ <span class="dropdown">
+ <span>
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <ul>
+ <li>
+ <span class="filter-collapse">
+ <i class="fa fa-compress fa-fw"></i> <a href="#" class="filter-collapse-link"><?= t('Collapse tasks') ?></a>
+ </span>
+ <span class="filter-expand" style="display: none">
+ <i class="fa fa-expand fa-fw"></i> <a href="#" class="filter-expand-link"><?= t('Expand tasks') ?></a>
+ </span>
+ </li>
+ <li>
+ <span class="filter-compact">
+ <i class="fa fa-th fa-fw"></i> <a href="#" class="filter-toggle-scrolling"><?= 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"><?= t('Horizontal scrolling') ?></a>
+ </span>
+ </li>
+ <?= $this->render('project/dropdown', array('project' => $project)) ?>
+ </ul>
+ </span>
+ </span>
+ </li>
+ <li>
+ <?= $this->form->select('user_id', $users, array(), array(), array('data-placeholder="'.t('Filter by user').'"', 'data-notfound="'.t('No results match:').'"'), 'apply-filters chosen-select') ?>
+ </li>
+ <li>
+ <?= $this->form->select('category_id', $categories, array(), array(), array('data-placeholder="'.t('Filter by category').'"', 'data-notfound="'.t('No results match:').'"'), 'apply-filters chosen-select') ?>
+ </li>
+ <li>
+ <select id="more-filters" multiple data-placeholder="<?= t('More filters') ?>" data-notfound="<?= t('No results match:') ?>" class="apply-filters hide-mobile">
+ <option value=""></option>
+ <option value="filter-due-date"><?= t('Filter by due date') ?></option>
+ <option value="filter-recent"><?= t('Filter recently updated') ?></option>
+ </select>
+ </li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/board/index.php b/app/Template/board/index.php
new file mode 100644
index 00000000..f87e0077
--- /dev/null
+++ b/app/Template/board/index.php
@@ -0,0 +1,18 @@
+<section id="main">
+
+ <?= $this->render('board/filters', array(
+ 'categories' => $categories_listing,
+ 'users' => $users,
+ 'project' => $project,
+ )) ?>
+
+ <?= $this->render('board/show', array(
+ 'project' => $project,
+ 'swimlanes' => $swimlanes,
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
+ 'board_private_refresh_interval' => $board_private_refresh_interval,
+ 'board_highlight_period' => $board_highlight_period,
+ )) ?>
+
+</section>
diff --git a/app/Template/board/public.php b/app/Template/board/public.php
new file mode 100644
index 00000000..9e5360ce
--- /dev/null
+++ b/app/Template/board/public.php
@@ -0,0 +1,13 @@
+<section id="main" class="public-board">
+
+ <?= $this->render('board/show', array(
+ 'project' => $project,
+ 'swimlanes' => $swimlanes,
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
+ 'board_private_refresh_interval' => $board_private_refresh_interval,
+ 'board_highlight_period' => $board_highlight_period,
+ 'not_editable' => true,
+ )) ?>
+
+</section> \ No newline at end of file
diff --git a/app/Template/board/show.php b/app/Template/board/show.php
new file mode 100644
index 00000000..3b10d82c
--- /dev/null
+++ b/app/Template/board/show.php
@@ -0,0 +1,32 @@
+<div id="board-container">
+ <?php if (isset($not_editable)): ?>
+ <table id="board" class="board-project-<?= $project['id'] ?>">
+ <?php else: ?>
+ <table id="board"
+ class="board-project-<?= $project['id'] ?>"
+ data-project-id="<?= $project['id'] ?>"
+ data-check-interval="<?= $board_private_refresh_interval ?>"
+ data-save-url="<?= $this->url->href('board', 'save', array('project_id' => $project['id'])) ?>"
+ data-check-url="<?= $this->url->href('board', 'check', array('project_id' => $project['id'], 'timestamp' => time())) ?>"
+ data-task-creation-url="<?= $this->url->href('task', 'create', array('project_id' => $project['id'])) ?>"
+ >
+ <?php endif ?>
+
+ <?php foreach ($swimlanes as $swimlane): ?>
+ <?php if (empty($swimlane['columns'])): ?>
+ <p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
+ <?php break ?>
+ <?php else: ?>
+ <?= $this->render('board/swimlane', array(
+ 'project' => $project,
+ 'swimlane' => $swimlane,
+ 'board_highlight_period' => $board_highlight_period,
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
+ 'hide_swimlane' => count($swimlanes) === 1,
+ 'not_editable' => isset($not_editable),
+ )) ?>
+ <?php endif ?>
+ <?php endforeach ?>
+ </table>
+</div> \ No newline at end of file
diff --git a/app/Template/board/subtasks.php b/app/Template/board/subtasks.php
new file mode 100644
index 00000000..950da925
--- /dev/null
+++ b/app/Template/board/subtasks.php
@@ -0,0 +1,7 @@
+<section id="tooltip-subtasks">
+<?php foreach ($subtasks as $subtask): ?>
+ <?= $this->subtask->toggleStatus($subtask, 'board') ?>
+ <?= $this->e(empty($subtask['username']) ? '' : ' ['.$this->user->getFullname($subtask).']') ?>
+ <br/>
+<?php endforeach ?>
+</section>
diff --git a/app/Template/board/swimlane.php b/app/Template/board/swimlane.php
new file mode 100644
index 00000000..201ee2fc
--- /dev/null
+++ b/app/Template/board/swimlane.php
@@ -0,0 +1,85 @@
+<tr id="swimlane-<?= $swimlane['id'] ?>">
+ <?php if (! $hide_swimlane): ?>
+ <th>
+ <?php if (! $not_editable && $swimlane['nb_tasks'] > 0): ?>
+ <a href="#" class="board-swimlane-toggle" data-swimlane-id="<?= $swimlane['id'] ?>">
+ <i class="fa fa-minus-circle hide-icon-swimlane-<?= $swimlane['id'] ?>"></i>
+ <i class="fa fa-plus-circle show-icon-swimlane-<?= $swimlane['id'] ?>" style="display: none"></i>
+ </a>
+ <span class="board-swimlane-toggle-title show-icon-swimlane-<?= $swimlane['id'] ?>"><?= $this->e($swimlane['name']) ?></span>
+ <?php endif ?>
+ </th>
+ <?php endif ?>
+
+ <?php foreach ($swimlane['columns'] as $column): ?>
+ <th class="board-column">
+ <?php if (! $not_editable): ?>
+ <div class="board-add-icon">
+ <?= $this->url->link('+', 'task', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'task-board-popover', t('Add a new task')) ?>
+ </div>
+ <?php endif ?>
+
+ <?= $this->e($column['title']) ?>
+
+ <?php if (! $not_editable && ! empty($column['description'])): ?>
+ <span class="column-tooltip pull-right" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?php endif ?>
+
+ <?php if (! empty($column['score'])): ?>
+ <span class="column-score pull-right" title="<?= t('Score') ?>">
+ <?= $column['score'] ?>&nbsp;
+ </span>
+ <?php endif ?>
+
+ <?php if ($column['task_limit']): ?>
+ <span title="<?= t('Task limit') ?>" class="task-limit">
+ (<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>/<?= $this->e($column['task_limit']) ?>)
+ </span>
+ <?php else: ?>
+ <span title="<?= t('Task count') ?>" class="task-count">
+ (<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>)
+ </span>
+ <?php endif ?>
+ </th>
+ <?php endforeach ?>
+</tr>
+<tr class="swimlane-row-<?= $swimlane['id'] ?>">
+
+ <?php if (! $hide_swimlane): ?>
+ <th class="board-swimlane-title">
+ <?= $this->e($swimlane['name']) ?>
+
+ <span title="<?= t('Task count') ?>" class="task-count">
+ (<span><?= $swimlane['nb_tasks'] ?></span>)
+ </span>
+ </th>
+ <?php endif ?>
+
+ <?php foreach ($swimlane['columns'] as $column): ?>
+
+ <?php if ($not_editable): ?>
+ <td>
+ <?php else: ?>
+ <td
+ id="column-<?= $column['id'] ?>"
+ class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>"
+ data-column-id="<?= $column['id'] ?>"
+ data-swimlane-id="<?= $swimlane['id'] ?>"
+ data-task-limit="<?= $column['task_limit'] ?>">
+ <?php endif ?>
+
+ <?php foreach ($column['tasks'] as $task): ?>
+ <?= $this->render($not_editable ? 'board/task_public' : 'board/task_private', array(
+ 'project' => $project,
+ 'task' => $task,
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
+ 'board_highlight_period' => $board_highlight_period,
+ 'not_editable' => $not_editable,
+ )) ?>
+ <?php endforeach ?>
+ </td>
+ <?php endforeach ?>
+</tr> \ No newline at end of file
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
new file mode 100644
index 00000000..5945d5af
--- /dev/null
+++ b/app/Template/board/task_footer.php
@@ -0,0 +1,61 @@
+<?php if (! empty($task['category_id'])): ?>
+<div class="task-board-category-container">
+ <span class="task-board-category">
+ <?php if ($not_editable): ?>
+ <?= $this->text->in($task['category_id'], $categories_listing) ?>
+ <?php else: ?>
+ <?= $this->url->link(
+ $this->text->in($task['category_id'], $categories_listing),
+ 'board',
+ 'changeCategory',
+ array('task_id' => $task['id'], 'project_id' => $task['project_id']),
+ false,
+ 'task-board-popover' . (isset($categories_description[$task['category_id']]) ? ' column-tooltip' : ''),
+ isset($categories_description[$task['category_id']]) ? $this->text->markdown($categories_description[$task['category_id']]) : t('Change category')
+ ) ?>
+ <?php endif ?>
+ </span>
+</div>
+<?php endif ?>
+
+<div class="task-board-icons">
+ <?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>&nbsp;<?= dt('%b %e', $task['date_due']) ?>
+ </span>
+ <?php endif ?>
+
+ <?php if ($task['recurrence_status'] == \Model\Task::RECURRING_STATUS_PENDING): ?>
+ <span title="<?= t('Recurrence') ?>" class="task-board-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>
+ <?php endif ?>
+
+ <?php if ($task['recurrence_status'] == \Model\Task::RECURRING_STATUS_PROCESSED): ?>
+ <span title="<?= t('Recurrence') ?>" class="task-board-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>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_links'])): ?>
+ <span title="<?= t('Links') ?>" class="task-board-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>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_subtasks'])): ?>
+ <span title="<?= t('Sub-Tasks') ?>" class="task-board-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>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_files'])): ?>
+ <span title="<?= t('Attachments') ?>" class="task-board-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>
+ <?php endif ?>
+
+ <?php if (! empty($task['nb_comments'])): ?>
+ <span title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>" class="task-board-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>
+ <?php endif ?>
+
+ <?php if (! empty($task['description'])): ?>
+ <span title="<?= t('Description') ?>" class="task-board-tooltip" data-href="<?= $this->url->href('board', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
+ <i class="fa fa-file-text-o"></i>
+ </span>
+ <?php endif ?>
+
+ <?php if ($task['score']): ?>
+ <span class="task-score"><?= $this->e($task['score']) ?></span>
+ <?php endif ?>
+</div>
diff --git a/app/Template/board/task_menu.php b/app/Template/board/task_menu.php
new file mode 100644
index 00000000..97c0f8dc
--- /dev/null
+++ b/app/Template/board/task_menu.php
@@ -0,0 +1,16 @@
+<span class="dropdown">
+ <span>
+ <a href="#" class="dropdown-menu"><?= '#'.$task['id'] ?></a>
+ <ul>
+ <li><i class="fa fa-user"></i> <?= $this->url->link(t('Change assignee'), 'board', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-tag"></i> <?= $this->url->link(t('Change category'), 'board', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-align-left"></i> <?= $this->url->link(t('Change description'), 'task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-pencil-square-o"></i> <?= $this->url->link(t('Edit this task'), 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-comment-o"></i> <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-code-fork"></i> <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-camera"></i> <?= $this->url->link(t('Add a screenshot'), 'board', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-refresh fa-rotate-90"></i> <?= $this->url->link(t('Edit recurrence'), 'task', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li>
+ <li><i class="fa fa-close"></i> <?= $this->url->link(t('Close this task'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'task-board-popover') ?></li>
+ </ul>
+ </span>
+</span>
diff --git a/app/Template/board/task_private.php b/app/Template/board/task_private.php
new file mode 100644
index 00000000..088f47bc
--- /dev/null
+++ b/app/Template/board/task_private.php
@@ -0,0 +1,50 @@
+<div class="task-board draggable-item color-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>"
+ data-task-id="<?= $task['id'] ?>"
+ data-owner-id="<?= $task['owner_id'] ?>"
+ data-category-id="<?= $task['category_id'] ?>"
+ data-due-date="<?= $task['date_due'] ?>"
+ data-task-url="<?= $this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
+
+ <?= $this->render('board/task_menu', array('task' => $task)) ?>
+
+ <div class="task-board-collapsed" style="display: none">
+ <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title') ?>
+ </div>
+
+ <div class="task-board-expanded">
+
+ <?php if ($task['reference']): ?>
+ <span class="task-board-reference" title="<?= t('Reference') ?>">
+ (<?= $task['reference'] ?>)
+ </span>
+ <?php endif ?>
+
+ <span class="task-board-user <?= $this->user->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>">
+ <?= $this->url->link(
+ (! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')),
+ 'board',
+ 'changeAssignee',
+ array('task_id' => $task['id'], 'project_id' => $task['project_id']),
+ false,
+ 'task-board-popover',
+ t('Change assignee')
+ ) ?>
+ </span>
+
+ <div class="task-board-days">
+ <span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->task->age($task['date_creation']) ?></span>
+ <span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->task->age($task['date_moved']) ?></span>
+ </div>
+
+ <div class="task-board-title">
+ <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
+ </div>
+
+ <?= $this->render('board/task_footer', array(
+ 'task' => $task,
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
+ 'not_editable' => $not_editable,
+ )) ?>
+ </div>
+</div>
diff --git a/app/Template/board/task_public.php b/app/Template/board/task_public.php
new file mode 100644
index 00000000..9ac6e570
--- /dev/null
+++ b/app/Template/board/task_public.php
@@ -0,0 +1,31 @@
+<div class="task-board color-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>">
+
+ <?= $this->url->link('#'.$task['id'], 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?>
+
+ <?php if ($task['reference']): ?>
+ <span class="task-board-reference" title="<?= t('Reference') ?>">
+ (<?= $task['reference'] ?>)
+ </span>
+ <?php endif ?>
+
+ &nbsp;-&nbsp;
+
+ <span class="task-board-user">
+ <?php if (! empty($task['owner_id'])): ?>
+ <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?>
+ <?php else: ?>
+ <span class="task-board-nobody"><?= t('Nobody assigned') ?></span>
+ <?php endif ?>
+ </span>
+
+ <div class="task-board-title">
+ <?= $this->url->link($this->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?>
+ </div>
+
+ <?= $this->render('board/task_footer', array(
+ 'task' => $task,
+ 'categories_listing' => $categories_listing,
+ 'categories_description' => $categories_description,
+ 'not_editable' => $not_editable,
+ )) ?>
+</div> \ No newline at end of file
diff --git a/app/Template/board/tasklinks.php b/app/Template/board/tasklinks.php
new file mode 100644
index 00000000..25aa91aa
--- /dev/null
+++ b/app/Template/board/tasklinks.php
@@ -0,0 +1,18 @@
+<div class="tooltip-tasklinks">
+ <ul>
+ <?php foreach($links as $link): ?>
+ <li>
+ <strong><?= t($link['label']) ?></strong>
+ <?= $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>
+ <?php endforeach ?>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/budget/breakdown.php b/app/Template/budget/breakdown.php
new file mode 100644
index 00000000..92561188
--- /dev/null
+++ b/app/Template/budget/breakdown.php
@@ -0,0 +1,30 @@
+<div class="page-header">
+ <h2><?= t('Cost breakdown') ?></h2>
+</div>
+
+<?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is nothing to show.') ?></p>
+<?php else: ?>
+ <table class="table-fixed">
+ <tr>
+ <th class="column-20"><?= $paginator->order(t('Task'), 'task_title') ?></th>
+ <th class="column-25"><?= $paginator->order(t('Subtask'), 'subtask_title') ?></th>
+ <th class="column-20"><?= $paginator->order(t('User'), 'username') ?></th>
+ <th class="column-10"><?= t('Cost') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Time spent'), \Model\SubtaskTimeTracking::TABLE.'.time_spent') ?></th>
+ <th class="column-15"><?= $paginator->order(t('Date'), 'start') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $record): ?>
+ <tr>
+ <td><?= $this->url->link($this->e($record['task_title']), 'task', 'show', array('project_id' => $project['id'], 'task_id' => $record['task_id'])) ?></td>
+ <td><?= $this->url->link($this->e($record['subtask_title']), 'task', 'show', array('project_id' => $project['id'], 'task_id' => $record['task_id'])) ?></td>
+ <td><?= $this->url->link($this->e($record['name'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?></td>
+ <td><?= n($record['cost']) ?></td>
+ <td><?= n($record['time_spent']).' '.t('hours') ?></td>
+ <td><?= dt('%B %e, %Y', $record['start']) ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/budget/create.php b/app/Template/budget/create.php
new file mode 100644
index 00000000..a563796d
--- /dev/null
+++ b/app/Template/budget/create.php
@@ -0,0 +1,47 @@
+<div class="page-header">
+ <h2><?= t('Budget lines') ?></h2>
+</div>
+
+<?php if (! empty($lines)): ?>
+<table class="table-fixed table-stripped">
+ <tr>
+ <th class="column-20"><?= t('Budget line') ?></th>
+ <th class="column-20"><?= t('Date') ?></th>
+ <th><?= t('Comment') ?></th>
+ <th><?= t('Action') ?></th>
+ </tr>
+ <?php foreach ($lines as $line): ?>
+ <tr>
+ <td><?= n($line['amount']) ?></td>
+ <td><?= dt('%B %e, %Y', strtotime($line['date'])) ?></td>
+ <td><?= $this->e($line['comment']) ?></td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'budget', 'confirm', array('project_id' => $project['id'], 'budget_id' => $line['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<h3><?= t('New budget line') ?></h3>
+<?php endif ?>
+
+<form method="post" action="<?= $this->url->href('budget', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('project_id', $values) ?>
+
+ <?= $this->form->label(t('Amount'), 'amount') ?>
+ <?= $this->form->text('amount', $values, $errors, array('required'), 'form-numeric') ?>
+
+ <?= $this->form->label(t('Date'), 'date') ?>
+ <?= $this->form->text('date', $values, $errors, array('required'), 'form-date') ?>
+
+ <?= $this->form->label(t('Comment'), 'comment') ?>
+ <?= $this->form->text('comment', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/budget/index.php b/app/Template/budget/index.php
new file mode 100644
index 00000000..4fe8ac69
--- /dev/null
+++ b/app/Template/budget/index.php
@@ -0,0 +1,33 @@
+<?= $this->asset->js('assets/js/vendor/d3.v3.4.8.min.js') ?>
+<?= $this->asset->js('assets/js/vendor/dimple.v2.1.2.min.js') ?>
+
+<div class="page-header">
+ <h2><?= t('Budget overview') ?></h2>
+</div>
+
+<?php if (! empty($daily_budget)): ?>
+<div id="budget-chart">
+ <div id="chart"
+ data-serie='<?= json_encode($daily_budget) ?>'
+ data-labels='<?= json_encode(array('in' => t('Budget line'), 'out' => t('Expenses'), 'left' => t('Remaining'), 'value' => t('Amount'), 'date' => t('Date'), 'type' => t('Type'))) ?>'></div>
+</div>
+<hr/>
+<table class="table-fixed table-stripped">
+ <tr>
+ <th><?= t('Date') ?></td>
+ <th><?= t('Budget line') ?></td>
+ <th><?= t('Expenses') ?></td>
+ <th><?= t('Remaining') ?></td>
+ </tr>
+ <?php foreach ($daily_budget as $line): ?>
+ <tr>
+ <td><?= dt('%B %e, %Y', strtotime($line['date'])) ?></td>
+ <td><?= n($line['in']) ?></td>
+ <td><?= n($line['out']) ?></td>
+ <td><?= n($line['left']) ?></td>
+ </tr>
+ <?php endforeach ?>
+</table>
+<?php else: ?>
+ <p class="alert"><?= t('There is not enough data to show something.') ?></p>
+<?php endif ?>
diff --git a/app/Template/budget/remove.php b/app/Template/budget/remove.php
new file mode 100644
index 00000000..a5b906a1
--- /dev/null
+++ b/app/Template/budget/remove.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Remove budget line') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this budget line?') ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'budget', 'remove', array('project_id' => $project['id'], 'budget_id' => $budget_id), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'budget', 'create', array('project_id' => $project['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/budget/sidebar.php b/app/Template/budget/sidebar.php
new file mode 100644
index 00000000..7740cf00
--- /dev/null
+++ b/app/Template/budget/sidebar.php
@@ -0,0 +1,14 @@
+<div class="sidebar">
+ <h2><?= t('Budget') ?></h2>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Budget overview'), 'budget', 'index', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Budget lines'), 'budget', 'create', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Cost breakdown'), 'budget', 'breakdown', array('project_id' => $project['id'])) ?>
+ </li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php
new file mode 100644
index 00000000..cf2a20ec
--- /dev/null
+++ b/app/Template/calendar/show.php
@@ -0,0 +1,46 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <span class="dropdown">
+ <span>
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <ul>
+ <?= $this->render('project/dropdown', array('project' => $project)) ?>
+ </ul>
+ </span>
+ </span>
+ </li>
+ <li>
+ <i class="fa fa-table fa-fw"></i>
+ <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-folder fa-fw"></i>
+ <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ </li>
+ </ul>
+ </div>
+ <section class="sidebar-container">
+
+ <?= $this->render('calendar/sidebar', array(
+ 'project' => $project,
+ 'users_list' => $users_list,
+ 'categories_list' => $categories_list,
+ 'columns_list' => $columns_list,
+ 'swimlanes_list' => $swimlanes_list,
+ 'colors_list' => $colors_list,
+ 'status_list' => $status_list
+ )) ?>
+
+ <div class="sidebar-content">
+ <div id="calendar"
+ data-project-id="<?= $project['id'] ?>"
+ data-save-url="<?= $this->url->href('calendar', 'save') ?>"
+ data-check-url="<?= $this->url->href('calendar', 'project', array('project_id' => $project['id'])) ?>"
+ data-check-interval="<?= $check_interval ?>"
+ >
+ </div>
+ </div>
+ </section>
+</section> \ No newline at end of file
diff --git a/app/Template/calendar/sidebar.php b/app/Template/calendar/sidebar.php
new file mode 100644
index 00000000..6c4fb5b0
--- /dev/null
+++ b/app/Template/calendar/sidebar.php
@@ -0,0 +1,40 @@
+<div class="sidebar">
+ <ul class="no-bullet">
+ <li>
+ <?= t('Filter by user') ?>
+ </li>
+ <li>
+ <?= $this->form->select('owner_id', $users_list, array(), array(), array(), 'calendar-filter') ?>
+ </li>
+ <li>
+ <?= t('Filter by category') ?>
+ </li>
+ <li>
+ <?= $this->form->select('category_id', $categories_list, array(), array(), array(), 'calendar-filter') ?>
+ </li>
+ <li>
+ <?= t('Filter by column') ?>
+ </li>
+ <li>
+ <?= $this->form->select('column_id', $columns_list, array(), array(), array(), 'calendar-filter') ?>
+ </li>
+ <li>
+ <?= t('Filter by swimlane') ?>
+ </li>
+ <li>
+ <?= $this->form->select('swimlane_id', $swimlanes_list, array(), array(), array(), 'calendar-filter') ?>
+ </li>
+ <li>
+ <?= t('Filter by color') ?>
+ </li>
+ <li>
+ <?= $this->form->select('color_id', $colors_list, array(), array(), array(), 'calendar-filter') ?>
+ </li>
+ <li>
+ <?= t('Filter by status') ?>
+ </li>
+ <li>
+ <?= $this->form->select('is_active', $status_list, array(), array(), array(), 'calendar-filter') ?>
+ </li>
+ </ul>
+</div>
diff --git a/app/Template/category/edit.php b/app/Template/category/edit.php
new file mode 100644
index 00000000..7d40fe65
--- /dev/null
+++ b/app/Template/category/edit.php
@@ -0,0 +1,38 @@
+<div class="page-header">
+ <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">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('project_id', $values) ?>
+
+ <?= $this->form->label(t('Category 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"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/category/index.php b/app/Template/category/index.php
new file mode 100644
index 00000000..dba537d0
--- /dev/null
+++ b/app/Template/category/index.php
@@ -0,0 +1,42 @@
+<?php if (! empty($categories)): ?>
+<div class="page-header">
+ <h2><?= t('Categories') ?></h2>
+</div>
+<table>
+ <tr>
+ <th><?= t('Category Name') ?></th>
+ <th><?= t('Actions') ?></th>
+ </tr>
+ <?php foreach ($categories as $category_id => $category_name): ?>
+ <tr>
+ <td><?= $this->e($category_name) ?></td>
+ <td>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Edit'), 'category', 'edit', array('project_id' => $project['id'], 'category_id' => $category_id)) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Remove'), 'category', 'confirm', array('project_id' => $project['id'], 'category_id' => $category_id)) ?>
+ </li>
+ </ul>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+<?php endif ?>
+
+<div class="page-header">
+ <h2><?= t('Add a new category') ?></h2>
+</div>
+<form method="post" action="<?= $this->url->href('category', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('project_id', $values) ?>
+
+ <?= $this->form->label(t('Category 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"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Templates/category_remove.php b/app/Template/category/remove.php
index cfc23e07..ce589785 100644
--- a/app/Templates/category_remove.php
+++ b/app/Template/category/remove.php
@@ -9,8 +9,9 @@
</p>
<div class="form-actions">
- <a href="?controller=category&amp;action=remove&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=category&amp;project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a>
+ <?= $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'])) ?>
</div>
</div>
</section> \ No newline at end of file
diff --git a/app/Template/column/edit.php b/app/Template/column/edit.php
new file mode 100644
index 00000000..4d9848a9
--- /dev/null
+++ b/app/Template/column/edit.php
@@ -0,0 +1,42 @@
+<div class="page-header">
+ <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">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $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"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/column/index.php b/app/Template/column/index.php
new file mode 100644
index 00000000..18e7f284
--- /dev/null
+++ b/app/Template/column/index.php
@@ -0,0 +1,89 @@
+<div class="page-header">
+ <h2><?= t('Edit the board for "%s"', $project['name']) ?></h2>
+</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>
+ <tr>
+ <th><?= t('Column title') ?></th>
+ <th><?= t('Task limit') ?></th>
+ <th><?= t('Actions') ?></th>
+ </tr>
+ <?php foreach ($columns as $column): ?>
+ <tr>
+ <td class="column-60"><?= $this->e($column['title']) ?>
+ <?php if (! empty($column['description'])): ?>
+ <span class="column-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">
+ <ul>
+ <li>
+ <?= $this->url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>
+ </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'])) ?>
+ </li>
+ </ul>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </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"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></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/Templates/board_remove.php b/app/Template/column/remove.php
index 4529063b..28d0928f 100644
--- a/app/Templates/board_remove.php
+++ b/app/Template/column/remove.php
@@ -9,7 +9,7 @@
</p>
<div class="form-actions">
- <?= Helper\a(t('Yes'), 'board', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?>
- <?= t('or') ?> <?= Helper\a(t('cancel'), 'board', 'edit', array('project_id' => $project['id'])) ?>
+ <?= $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'])) ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/comment/create.php b/app/Template/comment/create.php
new file mode 100644
index 00000000..8c66d9a4
--- /dev/null
+++ b/app/Template/comment/create.php
@@ -0,0 +1,40 @@
+<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">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('task_id', $values) ?>
+ <?= $this->form->hidden('user_id', $values) ?>
+
+ <div class="form-tabs">
+ <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 class="write-area">
+ <?= $this->form->textarea('comment', $values, $errors, array(! isset($skip_cancel) ? 'autofocus' : '', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?>
+ </div>
+ <div class="preview-area">
+ <div class="markdown"></div>
+ </div>
+ </div>
+
+ <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
+
+ <div class="form-actions">
+ <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 ?>
+ <?php endif ?>
+ </div>
+</form>
diff --git a/app/Template/comment/edit.php b/app/Template/comment/edit.php
new file mode 100644
index 00000000..d67aa387
--- /dev/null
+++ b/app/Template/comment/edit.php
@@ -0,0 +1,36 @@
+<div class="page-header">
+ <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">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('task_id', $values) ?>
+ <?= $this->form->hidden('user_id', $values) ?>
+
+ <div class="form-tabs">
+ <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 class="write-area">
+ <?= $this->form->textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?>
+ </div>
+ <div class="preview-area">
+ <div class="markdown"></div>
+ </div>
+ </div>
+
+ <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
+
+ <div class="form-actions">
+ <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'])) ?>
+ </div>
+</form>
diff --git a/app/Template/comment/forbidden.php b/app/Template/comment/forbidden.php
new file mode 100644
index 00000000..1e306d45
--- /dev/null
+++ b/app/Template/comment/forbidden.php
@@ -0,0 +1,7 @@
+<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
new file mode 100644
index 00000000..afc3346f
--- /dev/null
+++ b/app/Template/comment/remove.php
@@ -0,0 +1,17 @@
+<div class="page-header">
+ <h2><?= t('Remove a comment') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this comment?') ?>
+ </p>
+
+ <?= $this->render('comment/show', array('comment' => $comment, 'task' => $task, 'preview' => true)) ?>
+
+ <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'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php
new file mode 100644
index 00000000..35394ccb
--- /dev/null
+++ b/app/Template/comment/show.php
@@ -0,0 +1,52 @@
+<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>">
+
+ <p class="comment-title">
+ <?php if (! empty($comment['email'])): ?>
+ <?= $this->user->avatar($comment['email'], $comment['name'] ?: $comment['username']) ?>
+ <?php endif ?>
+ <span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %Y at %k:%M %p', $comment['date']) ?></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']))): ?>
+ <li>
+ <?= $this->url->link(t('remove'), 'comment', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('edit'), 'comment', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ <?php endif ?>
+
+ <div class="markdown">
+ <?php if (isset($is_public) && $is_public): ?>
+ <?= $this->text->markdown(
+ $comment['comment'],
+ array(
+ 'controller' => 'task',
+ 'action' => 'readonly',
+ 'params' => array(
+ 'token' => $project['token']
+ )
+ )
+ ) ?>
+ <?php else: ?>
+ <?= $this->text->markdown(
+ $comment['comment'],
+ array(
+ 'controller' => 'task',
+ 'action' => 'show',
+ 'params' => array(
+ 'project_id' => $task['project_id']
+ )
+ )
+ ) ?>
+ <?php endif ?>
+ </div>
+
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/config/about.php b/app/Template/config/about.php
new file mode 100644
index 00000000..a7098c1b
--- /dev/null
+++ b/app/Template/config/about.php
@@ -0,0 +1,57 @@
+<div class="page-header">
+ <h2><?= t('About') ?></h2>
+</div>
+<div class="listing">
+ <ul>
+ <li>
+ <?= t('Official website:') ?>
+ <a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a>
+ </li>
+ <li>
+ <?= t('Application version:') ?>
+ <strong><?= APP_VERSION ?></strong>
+ </li>
+ </ul>
+</div>
+<div class="page-header">
+ <h2><?= t('Database') ?></h2>
+</div>
+<div class="listing">
+ <ul>
+ <li>
+ <?= t('Database driver:') ?>
+ <strong><?= $this->e(DB_DRIVER) ?></strong>
+ </li>
+ <?php if (DB_DRIVER === 'sqlite'): ?>
+ <li>
+ <?= t('Database size:') ?>
+ <strong><?= $this->text->bytes($db_size) ?></strong>
+ </li>
+ <li>
+ <?= $this->url->link(t('Download the database'), 'config', 'downloadDb', array(), true) ?>&nbsp;
+ <?= t('(Gzip compressed Sqlite file)') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Optimize the database'), 'config', 'optimizeDb', array(), true) ?>&nbsp;
+ <?= t('(VACUUM command)') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+</div>
+<div class="page-header">
+ <h2><?= t('Keyboard shortcuts') ?></h2>
+</div>
+<div class="listing">
+ <h3><?= t('Board view') ?></h3>
+ <ul>
+ <li><?= t('New task') ?> = <strong>n</strong></li>
+ <li><?= t('Expand/collapse tasks') ?> = <strong>s</strong></li>
+ <li><?= t('Compact/wide view') ?> = <strong>c</strong></li>
+ </ul>
+ <h3><?= t('Application') ?></h3>
+ <ul>
+ <li><?= t('Open board switcher') ?> = <strong>b</strong></li>
+ <li><?= t('Close dialog box') ?> = <strong>ESC</strong></li>
+ <li><?= t('Submit a form') ?> = <strong>CTRL+ENTER</strong> <?= t('or') ?> <strong>⌘+ENTER</strong></li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/config/api.php b/app/Template/config/api.php
new file mode 100644
index 00000000..489f1968
--- /dev/null
+++ b/app/Template/config/api.php
@@ -0,0 +1,18 @@
+<div class="page-header">
+ <h2><?= t('API') ?></h2>
+</div>
+<section class="listing">
+ <ul>
+ <li>
+ <?= t('API token:') ?>
+ <strong><?= $this->e($values['api_token']) ?></strong>
+ </li>
+ <li>
+ <?= t('API endpoint:') ?>
+ <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().'jsonrpc.php' ?>">
+ </li>
+ <li>
+ <?= $this->url->link(t('Reset token'), 'config', 'token', array('type' => 'api'), true) ?>
+ </li>
+ </ul>
+</section> \ No newline at end of file
diff --git a/app/Template/config/application.php b/app/Template/config/application.php
new file mode 100644
index 00000000..7d4c811d
--- /dev/null
+++ b/app/Template/config/application.php
@@ -0,0 +1,30 @@
+<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->label(t('Language'), 'application_language') ?>
+ <?= $this->form->select('application_language', $languages, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Timezone'), 'application_timezone') ?>
+ <?= $this->form->select('application_timezone', $timezones, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Date format'), 'application_date_format') ?>
+ <?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?><br/>
+ <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('Custom Stylesheet'), 'application_stylesheet') ?>
+ <?= $this->form->textarea('application_stylesheet', $values, $errors) ?><br/>
+
+ <div class="form-actions">
+ <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
new file mode 100644
index 00000000..19a4bcd7
--- /dev/null
+++ b/app/Template/config/board.php
@@ -0,0 +1,25 @@
+<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/>
+ <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/>
+ <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/>
+ <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"/>
+ </div>
+</form>
+</section> \ No newline at end of file
diff --git a/app/Template/config/calendar.php b/app/Template/config/calendar.php
new file mode 100644
index 00000000..1cc985c8
--- /dev/null
+++ b/app/Template/config/calendar.php
@@ -0,0 +1,33 @@
+<div class="page-header">
+ <h2><?= t('Calendar settings') ?></h2>
+</div>
+<section>
+<form method="post" action="<?= $this->url->href('config', 'calendar') ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <h3><?= t('Project calendar view') ?></h3>
+ <div class="listing">
+ <?= $this->form->radios('calendar_project_tasks', array(
+ 'date_creation' => t('Show tasks based on the creation date'),
+ 'date_started' => t('Show tasks based on the start date'),
+ ), $values) ?>
+ </div>
+
+ <h3><?= t('User calendar view') ?></h3>
+ <div class="listing">
+ <?= $this->form->radios('calendar_user_tasks', array(
+ 'date_creation' => t('Show tasks based on the creation date'),
+ 'date_started' => t('Show tasks based on the start date'),
+ ), $values) ?>
+
+ <h4><?= t('Subtasks time tracking') ?></h4>
+ <?= $this->form->checkbox('calendar_user_subtasks_time_tracking', t('Show subtasks based on the time tracking'), 1, $values['calendar_user_subtasks_time_tracking'] == 1) ?>
+ <?= $this->form->checkbox('calendar_user_subtasks_forecast', t('Show subtask estimates (forecast of future work)'), 1, $values['calendar_user_subtasks_forecast'] == 1) ?>
+ </div>
+
+ <div class="form-actions">
+ <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
new file mode 100644
index 00000000..a1299806
--- /dev/null
+++ b/app/Template/config/integrations.php
@@ -0,0 +1,87 @@
+<div class="page-header">
+ <h2><?= t('Integration with third-party services') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->url->href('config', 'integrations') ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <h3><img src="assets/img/mailgun-icon.png"/>&nbsp;<?= t('Mailgun (incoming emails)') ?></h3>
+ <div class="listing">
+ <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'mailgun', array('token' => $values['webhook_token'])) ?>"/><br/>
+ <p class="form-help"><a href="http://kanboard.net/documentation/mailgun" target="_blank"><?= t('Help on Mailgun integration') ?></a></p>
+ </div>
+
+ <h3><img src="assets/img/sendgrid-icon.png"/>&nbsp;<?= t('Sendgrid (incoming emails)') ?></h3>
+ <div class="listing">
+ <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'sendgrid', array('token' => $values['webhook_token'])) ?>"/><br/>
+ <p class="form-help"><a href="http://kanboard.net/documentation/sendgrid" target="_blank"><?= t('Help on Sendgrid integration') ?></a></p>
+ </div>
+
+ <h3><img src="assets/img/postmark-icon.png"/>&nbsp;<?= t('Postmark (incoming emails)') ?></h3>
+ <div class="listing">
+ <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'postmark', array('token' => $values['webhook_token'])) ?>"/><br/>
+ <p class="form-help"><a href="http://kanboard.net/documentation/postmark" target="_blank"><?= t('Help on Postmark integration') ?></a></p>
+ </div>
+
+ <h3><img src="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) ?>
+ </div>
+
+ <h3><img src="assets/img/jabber-icon.png"/> <?= t('Jabber (XMPP)') ?></h3>
+ <div class="listing">
+ <?= $this->form->checkbox('integration_jabber', t('Send notifications to Jabber'), 1, $values['integration_jabber'] == 1) ?>
+
+ <?= $this->form->label(t('XMPP server address'), 'integration_jabber_server') ?>
+ <?= $this->form->text('integration_jabber_server', $values, $errors, array('placeholder="tcp://myserver:5222"')) ?>
+ <p class="form-help"><?= t('The server address must use this format: "tcp://hostname:5222"') ?></p>
+
+ <?= $this->form->label(t('Jabber domain'), 'integration_jabber_domain') ?>
+ <?= $this->form->text('integration_jabber_domain', $values, $errors, array('placeholder="example.com"')) ?>
+
+ <?= $this->form->label(t('Username'), 'integration_jabber_username') ?>
+ <?= $this->form->text('integration_jabber_username', $values, $errors) ?>
+
+ <?= $this->form->label(t('Password'), 'integration_jabber_password') ?>
+ <?= $this->form->password('integration_jabber_password', $values, $errors) ?>
+
+ <?= $this->form->label(t('Jabber nickname'), 'integration_jabber_nickname') ?>
+ <?= $this->form->text('integration_jabber_nickname', $values, $errors) ?>
+
+ <?= $this->form->label(t('Multi-user chat room'), 'integration_jabber_room') ?>
+ <?= $this->form->text('integration_jabber_room', $values, $errors, array('placeholder="myroom@conference.example.com"')) ?>
+
+ <p class="form-help"><a href="http://kanboard.net/documentation/jabber" target="_blank"><?= t('Help on Jabber integration') ?></a></p>
+ </div>
+
+ <h3><img src="assets/img/hipchat-icon.png"/> <?= t('Hipchat') ?></h3>
+ <div class="listing">
+ <?= $this->form->checkbox('integration_hipchat', t('Send notifications to Hipchat'), 1, $values['integration_hipchat'] == 1) ?>
+
+ <?= $this->form->label(t('API URL'), 'integration_hipchat_api_url') ?>
+ <?= $this->form->text('integration_hipchat_api_url', $values, $errors) ?>
+
+ <?= $this->form->label(t('Room API ID or name'), 'integration_hipchat_room_id') ?>
+ <?= $this->form->text('integration_hipchat_room_id', $values, $errors) ?>
+
+ <?= $this->form->label(t('Room notification token'), 'integration_hipchat_room_token') ?>
+ <?= $this->form->text('integration_hipchat_room_token', $values, $errors) ?>
+
+ <p class="form-help"><a href="http://kanboard.net/documentation/hipchat" target="_blank"><?= t('Help on Hipchat integration') ?></a></p>
+ </div>
+
+ <h3><i class="fa fa-slack fa-fw"></i>&nbsp;<?= t('Slack') ?></h3>
+ <div class="listing">
+ <?= $this->form->checkbox('integration_slack_webhook', t('Send notifications to a Slack channel'), 1, $values['integration_slack_webhook'] == 1) ?>
+
+ <?= $this->form->label(t('Webhook URL'), 'integration_slack_webhook_url') ?>
+ <?= $this->form->text('integration_slack_webhook_url', $values, $errors) ?>
+
+ <p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p>
+ </div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/config/layout.php b/app/Template/config/layout.php
new file mode 100644
index 00000000..028f138c
--- /dev/null
+++ b/app/Template/config/layout.php
@@ -0,0 +1,10 @@
+<section id="main">
+ <section class="sidebar-container" id="config-section">
+
+ <?= $this->render('config/sidebar') ?>
+
+ <div class="sidebar-content">
+ <?= $config_content_for_layout ?>
+ </div>
+ </section>
+</section> \ No newline at end of file
diff --git a/app/Template/config/project.php b/app/Template/config/project.php
new file mode 100644
index 00000000..90dd9c8e
--- /dev/null
+++ b/app/Template/config/project.php
@@ -0,0 +1,24 @@
+<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() ?>
+
+ <?= $this->form->label(t('Default columns for new projects (Comma-separated)'), 'board_columns') ?>
+ <?= $this->form->text('board_columns', $values, $errors) ?><br/>
+ <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/>
+ <p class="form-help"><?= t('Example: "Bug, Feature Request, Improvement"') ?></p>
+
+ <?= $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('Enable time tracking for subtasks'), 1, $values['subtask_time_tracking'] == 1) ?>
+
+ <div class="form-actions">
+ <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
new file mode 100644
index 00000000..7f946dee
--- /dev/null
+++ b/app/Template/config/sidebar.php
@@ -0,0 +1,35 @@
+<div class="sidebar">
+ <h2><?= t('Actions') ?></h2>
+ <ul>
+ <li>
+ <?= $this->url->link(t('About'), 'config', 'index') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Application settings'), 'config', 'application') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Project settings'), 'config', 'project') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Board settings'), 'config', 'board') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Calendar settings'), 'config', 'calendar') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Link settings'), 'link', 'index') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Currency rates'), 'currency', 'index') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Integrations'), 'config', 'integrations') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Webhooks'), 'config', 'webhook') ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('API'), 'config', 'api') ?>
+ </li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/config/webhook.php b/app/Template/config/webhook.php
new file mode 100644
index 00000000..73ca3598
--- /dev/null
+++ b/app/Template/config/webhook.php
@@ -0,0 +1,35 @@
+<div class="page-header">
+ <h2><?= t('Webhook settings') ?></h2>
+</div>
+<section>
+<form method="post" action="<?= $this->url->href('config', 'webhook') ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Webhook URL'), 'webhook_url') ?>
+ <?= $this->form->text('webhook_url', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+</section>
+
+<div class="page-header">
+ <h2><?= t('URL and token') ?></h2>
+</div>
+<section class="listing">
+ <ul>
+ <li>
+ <?= t('Webhook token:') ?>
+ <strong><?= $this->e($values['webhook_token']) ?></strong>
+ </li>
+ <li>
+ <?= t('URL for task creation:') ?>
+ <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'task', array('token' => $values['webhook_token'])) ?>">
+ </li>
+ <li>
+ <?= $this->url->link(t('Reset token'), 'config', 'token', array('type' => 'webhook'), true) ?>
+ </li>
+ </ul>
+</section> \ No newline at end of file
diff --git a/app/Template/currency/index.php b/app/Template/currency/index.php
new file mode 100644
index 00000000..f72c5700
--- /dev/null
+++ b/app/Template/currency/index.php
@@ -0,0 +1,56 @@
+<div class="page-header">
+ <h2><?= t('Currency rates') ?></h2>
+</div>
+
+<?php if (! empty($rates)): ?>
+
+<table class="table-stripped">
+ <tr>
+ <th class="column-35"><?= t('Currency') ?></th>
+ <th><?= t('Rate') ?></th>
+ </tr>
+ <?php foreach ($rates as $rate): ?>
+ <tr>
+ <td>
+ <strong><?= $this->e($rate['currency']) ?></strong>
+ </td>
+ <td>
+ <?= n($rate['rate']) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<hr/>
+<h3><?= t('Change reference currency') ?></h3>
+<?php endif ?>
+<form method="post" action="<?= $this->url->href('currency', 'reference') ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Reference currency'), 'application_currency') ?>
+ <?= $this->form->select('application_currency', $currencies, $config_values, $errors) ?><br/>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<hr/>
+<h3><?= t('Add a new currency rate') ?></h3>
+<form method="post" action="<?= $this->url->href('currency', 'create') ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Currency'), 'currency') ?>
+ <?= $this->form->select('currency', $currencies, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Rate'), 'rate') ?>
+ <?= $this->form->text('rate', $values, $errors, array(), 'form-numeric') ?><br/>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<p class="alert alert-info"><?= t('Currency rates are used to calculate project budget.') ?></p>
diff --git a/app/Template/event/comment_create.php b/app/Template/event/comment_create.php
new file mode 100644
index 00000000..462f15ca
--- /dev/null
+++ b/app/Template/event/comment_create.php
@@ -0,0 +1,12 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s commented 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>
+<div class="activity-description">
+ <em><?= $this->e($task['title']) ?></em><br/>
+ <div class="markdown"><?= $this->text->markdown($comment['comment']) ?></div>
+</div> \ No newline at end of file
diff --git a/app/Template/event/comment_update.php b/app/Template/event/comment_update.php
new file mode 100644
index 00000000..0cb10bf6
--- /dev/null
+++ b/app/Template/event/comment_update.php
@@ -0,0 +1,11 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s updated a comment on 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>
+<div class="activity-description">
+ <em><?= $this->e($task['title']) ?></em><br/>
+</div> \ No newline at end of file
diff --git a/app/Templates/project_events.php b/app/Template/event/events.php
index 1b606414..2dc79871 100644
--- a/app/Templates/project_events.php
+++ b/app/Template/event/events.php
@@ -5,11 +5,11 @@
<?php foreach ($events as $event): ?>
<div class="activity-event">
<p class="activity-datetime">
- <?php if (Helper\contains($event['event_name'], 'subtask')): ?>
+ <?php if ($this->text->contains($event['event_name'], 'subtask')): ?>
<i class="fa fa-tasks"></i>
- <?php elseif (Helper\contains($event['event_name'], 'task')): ?>
+ <?php elseif ($this->text->contains($event['event_name'], 'task')): ?>
<i class="fa fa-newspaper-o"></i>
- <?php elseif (Helper\contains($event['event_name'], 'comment')): ?>
+ <?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']) ?>
diff --git a/app/Templates/event_subtask_create.php b/app/Template/event/subtask_create.php
index 664e9da2..ca23aa9c 100644
--- a/app/Templates/event_subtask_create.php
+++ b/app/Template/event/subtask_create.php
@@ -1,12 +1,17 @@
+<?= $this->user->avatar($email, $author) ?>
+
<p class="activity-title">
- <?= e('%s created a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
+ <?= e('%s created a subtask for 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>
<div class="activity-description">
- <p><em><?= Helper\escape($task['title']) ?></em></p>
+ <p><em><?= $this->e($task['title']) ?></em></p>
<ul>
<li>
- <?= Helper\escape($subtask['title']) ?> (<strong><?= Helper\escape($subtask['status_name']) ?></strong>)
+ <?= $this->e($subtask['title']) ?> (<strong><?= $this->e($subtask['status_name']) ?></strong>)
</li>
<li>
<?php if ($subtask['username']): ?>
diff --git a/app/Templates/event_subtask_update.php b/app/Template/event/subtask_update.php
index 96a589dd..11a778de 100644
--- a/app/Templates/event_subtask_update.php
+++ b/app/Template/event/subtask_update.php
@@ -1,12 +1,17 @@
+<?= $this->user->avatar($email, $author) ?>
+
<p class="activity-title">
- <?= e('%s updated a subtask for the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
+ <?= e('%s updated a subtask for 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>
<div class="activity-description">
- <p><em><?= Helper\escape($task['title']) ?></em></p>
+ <p><em><?= $this->e($task['title']) ?></em></p>
<ul>
<li>
- <?= Helper\escape($subtask['title']) ?> (<strong><?= Helper\escape($subtask['status_name']) ?></strong>)
+ <?= $this->e($subtask['title']) ?> (<strong><?= $this->e($subtask['status_name']) ?></strong>)
</li>
<li>
<?php if ($subtask['username']): ?>
diff --git a/app/Template/event/task_assignee_change.php b/app/Template/event/task_assignee_change.php
new file mode 100644
index 00000000..cdec8743
--- /dev/null
+++ b/app/Template/event/task_assignee_change.php
@@ -0,0 +1,18 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?php $assignee = $task['assignee_name'] ?: $task['assignee_username'] ?>
+
+ <?php if (! empty($assignee)): ?>
+ <?= e('%s changed the assignee of the task %s to %s',
+ $this->e($author),
+ $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
+ $this->e($assignee)
+ ) ?>
+ <?php else: ?>
+ <?= e('%s remove the assignee of 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']))) ?>
+ <?php endif ?>
+</p>
+<p class="activity-description">
+ <em><?= $this->e($task['title']) ?></em>
+</p> \ No newline at end of file
diff --git a/app/Template/event/task_close.php b/app/Template/event/task_close.php
new file mode 100644
index 00000000..3d8670a6
--- /dev/null
+++ b/app/Template/event/task_close.php
@@ -0,0 +1,11 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s closed 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($task['title']) ?></em>
+</p> \ No newline at end of file
diff --git a/app/Template/event/task_create.php b/app/Template/event/task_create.php
new file mode 100644
index 00000000..773f401c
--- /dev/null
+++ b/app/Template/event/task_create.php
@@ -0,0 +1,11 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s created 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($task['title']) ?></em>
+</p> \ No newline at end of file
diff --git a/app/Template/event/task_move_column.php b/app/Template/event/task_move_column.php
new file mode 100644
index 00000000..ca482e46
--- /dev/null
+++ b/app/Template/event/task_move_column.php
@@ -0,0 +1,12 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s moved the task %s to the column "%s"',
+ $this->e($author),
+ $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
+ $this->e($task['column_title'])
+ ) ?>
+</p>
+<p class="activity-description">
+ <em><?= $this->e($task['title']) ?></em>
+</p> \ No newline at end of file
diff --git a/app/Template/event/task_move_position.php b/app/Template/event/task_move_position.php
new file mode 100644
index 00000000..dcdd3e1b
--- /dev/null
+++ b/app/Template/event/task_move_position.php
@@ -0,0 +1,13 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s moved the task %s to the position #%d in the column "%s"',
+ $this->e($author),
+ $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
+ $task['position'],
+ $this->e($task['column_title'])
+ ) ?>
+</p>
+<p class="activity-description">
+ <em><?= $this->e($task['title']) ?></em>
+</p> \ No newline at end of file
diff --git a/app/Template/event/task_open.php b/app/Template/event/task_open.php
new file mode 100644
index 00000000..11fec64b
--- /dev/null
+++ b/app/Template/event/task_open.php
@@ -0,0 +1,11 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s opened 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($task['title']) ?></em>
+</p> \ No newline at end of file
diff --git a/app/Template/event/task_update.php b/app/Template/event/task_update.php
new file mode 100644
index 00000000..7d036d43
--- /dev/null
+++ b/app/Template/event/task_update.php
@@ -0,0 +1,11 @@
+<?= $this->user->avatar($email, $author) ?>
+
+<p class="activity-title">
+ <?= e('%s updated 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($task['title']) ?></em>
+</p> \ No newline at end of file
diff --git a/app/Template/export/sidebar.php b/app/Template/export/sidebar.php
new file mode 100644
index 00000000..f93dcafb
--- /dev/null
+++ b/app/Template/export/sidebar.php
@@ -0,0 +1,17 @@
+<div class="sidebar">
+ <h2><?= t('Exports') ?></h2>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Tasks'), 'export', 'tasks', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Subtasks'), 'export', 'subtasks', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Task transitions'), 'export', 'transitions', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Daily project summary'), 'export', 'summary', array('project_id' => $project['id'])) ?>
+ </li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/export/subtasks.php b/app/Template/export/subtasks.php
new file mode 100644
index 00000000..4aad2641
--- /dev/null
+++ b/app/Template/export/subtasks.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <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>
+
+<form method="get" action="?" autocomplete="off">
+
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $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->label(t('End Date'), 'to') ?>
+ <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
+
+ <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"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/export/summary.php b/app/Template/export/summary.php
new file mode 100644
index 00000000..ffbd6ac2
--- /dev/null
+++ b/app/Template/export/summary.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <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>
+
+<form method="get" action="?" autocomplete="off">
+
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $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->label(t('End Date'), 'to') ?>
+ <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
+
+ <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"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/export/tasks.php b/app/Template/export/tasks.php
new file mode 100644
index 00000000..c74c8f98
--- /dev/null
+++ b/app/Template/export/tasks.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <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>
+
+<form method="get" action="?" autocomplete="off">
+
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $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->label(t('End Date'), 'to') ?>
+ <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
+
+ <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"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/export/transitions.php b/app/Template/export/transitions.php
new file mode 100644
index 00000000..bf6ef249
--- /dev/null
+++ b/app/Template/export/transitions.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <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>
+
+<form method="get" action="?" autocomplete="off">
+
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $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->label(t('End Date'), 'to') ?>
+ <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?>
+
+ <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"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/file/new.php b/app/Template/file/new.php
new file mode 100644
index 00000000..a1a59eae
--- /dev/null
+++ b/app/Template/file/new.php
@@ -0,0 +1,14 @@
+<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
new file mode 100644
index 00000000..3df012b6
--- /dev/null
+++ b/app/Template/file/open.php
@@ -0,0 +1,6 @@
+<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/remove.php b/app/Template/file/remove.php
new file mode 100644
index 00000000..37f648eb
--- /dev/null
+++ b/app/Template/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'), 'file', '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'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/file/screenshot.php b/app/Template/file/screenshot.php
new file mode 100644
index 00000000..89d9324c
--- /dev/null
+++ b/app/Template/file/screenshot.php
@@ -0,0 +1,17 @@
+<div class="page-header">
+ <h2><?= t('Add a screenshot') ?></h2>
+</div>
+
+<div id="screenshot-zone">
+ <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">
+ <input type="hidden" name="screenshot"/>
+ <?= $this->form->csrf() ?>
+ <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']), false, 'close-popover') ?>
+ </div>
+</form>
diff --git a/app/Template/file/show.php b/app/Template/file/show.php
new file mode 100644
index 00000000..7d5dc96f
--- /dev/null
+++ b/app/Template/file/show.php
@@ -0,0 +1,56 @@
+<?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('width' => 250, 'height' => 100, '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="column-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', '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="column-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/hourlyrate/index.php b/app/Template/hourlyrate/index.php
new file mode 100644
index 00000000..af305d07
--- /dev/null
+++ b/app/Template/hourlyrate/index.php
@@ -0,0 +1,46 @@
+<div class="page-header">
+ <h2><?= t('Hourly rates') ?></h2>
+</div>
+
+<?php if (! empty($rates)): ?>
+
+<table>
+ <tr>
+ <th><?= t('Hourly rate') ?></th>
+ <th><?= t('Currency') ?></th>
+ <th><?= t('Effective date') ?></th>
+ <th><?= t('Action') ?></th>
+ </tr>
+ <?php foreach ($rates as $rate): ?>
+ <tr>
+ <td><?= n($rate['rate']) ?></td>
+ <td><?= $rate['currency'] ?></td>
+ <td><?= dt('%b %e, %Y', $rate['date_effective']) ?></td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'hourlyrate', 'confirm', array('user_id' => $user['id'], 'rate_id' => $rate['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<h3><?= t('Add new rate') ?></h3>
+<?php endif ?>
+
+<form method="post" action="<?= $this->url->href('hourlyrate', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->hidden('user_id', $values) ?>
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Hourly rate'), 'rate') ?>
+ <?= $this->form->text('rate', $values, $errors, array('required'), 'form-numeric') ?>
+
+ <?= $this->form->label(t('Currency'), 'currency') ?>
+ <?= $this->form->select('currency', $currencies_list, $values, $errors, array('required')) ?>
+
+ <?= $this->form->label(t('Effective date'), 'date_effective') ?>
+ <?= $this->form->text('date_effective', $values, $errors, array('required'), 'form-date') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
diff --git a/app/Template/hourlyrate/remove.php b/app/Template/hourlyrate/remove.php
new file mode 100644
index 00000000..121436e4
--- /dev/null
+++ b/app/Template/hourlyrate/remove.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Remove hourly rate') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this hourly rate?') ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'hourlyrate', 'remove', array('user_id' => $user['id'], 'rate_id' => $rate_id), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'hourlyrate', 'index', array('user_id' => $user['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/layout.php b/app/Template/layout.php
new file mode 100644
index 00000000..cf74c8ab
--- /dev/null
+++ b/app/Template/layout.php
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <meta name="mobile-web-app-capable" content="yes">
+ <meta name="robots" content="noindex,nofollow">
+
+ <?php if (isset($board_public_refresh_interval)): ?>
+ <meta http-equiv="refresh" content="<?= $board_public_refresh_interval ?>">
+ <?php endif ?>
+
+ <?php if (! isset($not_editable)): ?>
+ <?= $this->asset->js('assets/js/app.js') ?>
+ <?php endif ?>
+
+ <?= $this->asset->css($this->url->href('app', 'colors'), false, 'all') ?>
+ <?= $this->asset->css('assets/css/app.css') ?>
+ <?= $this->asset->css('assets/css/print.css', true, 'print') ?>
+ <?= $this->asset->customCss() ?>
+
+ <link rel="icon" type="image/png" href="assets/img/favicon.png">
+ <link rel="apple-touch-icon" href="assets/img/touch-icon-iphone.png">
+ <link rel="apple-touch-icon" sizes="72x72" href="assets/img/touch-icon-ipad.png">
+ <link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png">
+ <link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png">
+
+ <title><?= isset($title) ? $this->e($title) : 'Kanboard' ?></title>
+ </head>
+ <body data-status-url="<?= $this->url->href('app', 'status') ?>"
+ data-login-url="<?= $this->url->href('auth', 'login') ?>"
+ data-timezone="<?= $this->app->getTimezone() ?>"
+ data-js-lang="<?= $this->app->jsLang() ?>">
+
+ <?php if (isset($no_layout) && $no_layout): ?>
+ <?= $content_for_layout ?>
+ <?php else: ?>
+ <header>
+ <nav>
+ <h1><?= $this->url->link('K<span>B</span>', 'app', 'index', array(), false, 'logo', t('Dashboard')).' '.$this->text->truncate($this->e($title)) ?>
+ <?php if (! empty($description)): ?>
+ <span class="column-tooltip" title='<?= $this->e($this->text->markdown($description)) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?php endif ?>
+ </h1>
+ <ul>
+ <?php if (isset($board_selector) && ! empty($board_selector)): ?>
+ <li>
+ <select id="board-selector" data-notfound="<?= t('No results match:') ?>" data-placeholder="<?= t('Display another project') ?>" data-board-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>">
+ <option value=""></option>
+ <?php foreach($board_selector as $board_id => $board_name): ?>
+ <option value="<?= $board_id ?>"><?= $this->e($board_name) ?></option>
+ <?php endforeach ?>
+ </select>
+ </li>
+ <?php endif ?>
+ <li>
+ <?= $this->url->link(t('Logout'), 'auth', 'logout') ?>
+ <span class="username hide-tablet">(<?= $this->user->getProfileLink() ?>)</span>
+ </li>
+ </ul>
+ </nav>
+ </header>
+ <section class="page">
+ <?= $this->app->flashMessage() ?>
+ <?= $content_for_layout ?>
+ </section>
+ <?php endif ?>
+ </body>
+</html>
diff --git a/app/Template/link/create.php b/app/Template/link/create.php
new file mode 100644
index 00000000..2b4ac62c
--- /dev/null
+++ b/app/Template/link/create.php
@@ -0,0 +1,18 @@
+<div class="page-header">
+ <h2><?= t('Add a new link') ?></h2>
+</div>
+
+<form action="<?= $this->url->href('link', 'save') ?>" method="post" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Label'), 'label') ?>
+ <?= $this->form->text('label', $values, $errors, array('required')) ?>
+
+ <?= $this->form->label(t('Opposite label'), 'opposite_label') ?>
+ <?= $this->form->text('opposite_label', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/link/edit.php b/app/Template/link/edit.php
new file mode 100644
index 00000000..516de464
--- /dev/null
+++ b/app/Template/link/edit.php
@@ -0,0 +1,21 @@
+<div class="page-header">
+ <h2><?= t('Link modification') ?></h2>
+</div>
+
+<form action="<?= $this->url->href('link', 'update', array('link_id' => $link['id'])) ?>" method="post" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+
+ <?= $this->form->label(t('Label'), 'label') ?>
+ <?= $this->form->text('label', $values, $errors, array('required')) ?>
+
+ <?= $this->form->label(t('Opposite label'), 'opposite_id') ?>
+ <?= $this->form->select('opposite_id', $labels, $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'link', 'index') ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/link/index.php b/app/Template/link/index.php
new file mode 100644
index 00000000..1475bd50
--- /dev/null
+++ b/app/Template/link/index.php
@@ -0,0 +1,33 @@
+<div class="page-header">
+ <h2><?= t('Link labels') ?></h2>
+</div>
+<?php if (! empty($links)): ?>
+<table>
+ <tr>
+ <th class="column-70"><?= t('Link labels') ?></th>
+ <th><?= t('Actions') ?></th>
+ </tr>
+ <?php foreach ($links as $link): ?>
+ <tr>
+ <td>
+ <strong><?= t($link['label']) ?></strong>
+
+ <?php if (! empty($link['opposite_label'])): ?>
+ | <?= t($link['opposite_label']) ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <ul>
+ <?= $this->url->link(t('Edit'), 'link', 'edit', array('link_id' => $link['id'])) ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('Remove'), 'link', 'confirm', array('link_id' => $link['id'])) ?>
+ </ul>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+<?php else: ?>
+ <?= t('There is no link.') ?>
+<?php endif ?>
+
+<?= $this->render('link/create', array('values' => $values, 'errors' => $errors)) ?> \ No newline at end of file
diff --git a/app/Template/link/remove.php b/app/Template/link/remove.php
new file mode 100644
index 00000000..12ca14bb
--- /dev/null
+++ b/app/Template/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['label']) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'link', 'remove', array('link_id' => $link['id']), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'link', 'index') ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/notification/comment_create.php b/app/Template/notification/comment_create.php
new file mode 100644
index 00000000..747c4f43
--- /dev/null
+++ b/app/Template/notification/comment_create.php
@@ -0,0 +1,7 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<h3><?= t('New comment posted by %s', $comment['name'] ?: $comment['username']) ?></h3>
+
+<?= $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/comment_update.php b/app/Template/notification/comment_update.php
new file mode 100644
index 00000000..a15e5d6d
--- /dev/null
+++ b/app/Template/notification/comment_update.php
@@ -0,0 +1,7 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<h3><?= t('Comment updated') ?></h3>
+
+<?= $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/file_create.php b/app/Template/notification/file_create.php
new file mode 100644
index 00000000..63f7d1b8
--- /dev/null
+++ b/app/Template/notification/file_create.php
@@ -0,0 +1,5 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<p><?= t('New attachment added "%s"', $file['name']) ?></p>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/notification/footer.php b/app/Template/notification/footer.php
new file mode 100644
index 00000000..7041c43b
--- /dev/null
+++ b/app/Template/notification/footer.php
@@ -0,0 +1,6 @@
+<hr/>
+Kanboard
+
+<?php if (isset($application_url) && ! empty($application_url)): ?>
+ - <a href="<?= $application_url.$this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= t('view the task on Kanboard') ?></a>.
+<?php endif ?>
diff --git a/app/Template/notification/subtask_create.php b/app/Template/notification/subtask_create.php
new file mode 100644
index 00000000..e1c62b73
--- /dev/null
+++ b/app/Template/notification/subtask_create.php
@@ -0,0 +1,17 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<h3><?= t('New sub-task') ?></h3>
+
+<ul>
+ <li><?= t('Title:') ?> <?= $this->e($subtask['title']) ?></li>
+ <li><?= t('Status:') ?> <?= $this->e($subtask['status_name']) ?></li>
+ <li><?= t('Assignee:') ?> <?= $this->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li>
+ <li>
+ <?= t('Time tracking:') ?>
+ <?php if (! empty($subtask['time_estimated'])): ?>
+ <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
+ <?php endif ?>
+ </li>
+</ul>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/notification/subtask_update.php b/app/Template/notification/subtask_update.php
new file mode 100644
index 00000000..cfde9db6
--- /dev/null
+++ b/app/Template/notification/subtask_update.php
@@ -0,0 +1,21 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<h3><?= t('Sub-task updated') ?></h3>
+
+<ul>
+ <li><?= t('Title:') ?> <?= $this->e($subtask['title']) ?></li>
+ <li><?= t('Status:') ?> <?= $this->e($subtask['status_name']) ?></li>
+ <li><?= t('Assignee:') ?> <?= $this->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li>
+ <li>
+ <?= t('Time tracking:') ?>
+ <?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>
+</ul>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_task_assignee_change.php b/app/Template/notification/task_assignee_change.php
index d23f769e..c9729ac9 100644
--- a/app/Templates/notification_task_assignee_change.php
+++ b/app/Template/notification/task_assignee_change.php
@@ -1,4 +1,4 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
<ul>
<li>
@@ -14,7 +14,7 @@
<?php if (! empty($task['description'])): ?>
<h2><?= t('Description') ?></h2>
- <?= Helper\markdown($task['description']) ?: t('There is no description.') ?>
+ <?= $this->text->markdown($task['description']) ?: t('There is no description.') ?>
<?php endif ?>
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/notification/task_close.php b/app/Template/notification/task_close.php
new file mode 100644
index 00000000..463223a0
--- /dev/null
+++ b/app/Template/notification/task_close.php
@@ -0,0 +1,5 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<p><?= t('The task #%d have been closed.', $task['id']) ?></p>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_task_update.php b/app/Template/notification/task_create.php
index b3c07911..1d834d44 100644
--- a/app/Templates/notification_task_update.php
+++ b/app/Template/notification/task_create.php
@@ -1,4 +1,4 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
<ul>
<li>
@@ -9,14 +9,14 @@
<strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong>
</li>
<?php endif ?>
- <?php if ($task['creator_username']): ?>
+ <?php if (! empty($task['creator_username'])): ?>
<li>
<?= t('Created by %s', $task['creator_name'] ?: $task['creator_username']) ?>
</li>
<?php endif ?>
<li>
<strong>
- <?php if ($task['assignee_username']): ?>
+ <?php if (! empty($task['assignee_username'])): ?>
<?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?>
<?php else: ?>
<?= t('There is nobody assigned') ?>
@@ -25,19 +25,19 @@
</li>
<li>
<?= t('Column on the board:') ?>
- <strong><?= Helper\escape($task['column_title']) ?></strong>
+ <strong><?= $this->e($task['column_title']) ?></strong>
</li>
- <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li>
- <?php if ($task['category_name']): ?>
+ <li><?= t('Task position:').' '.$this->e($task['position']) ?></li>
+ <?php if (! empty($task['category_name'])): ?>
<li>
- <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong>
+ <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong>
</li>
<?php endif ?>
</ul>
<?php if (! empty($task['description'])): ?>
<h2><?= t('Description') ?></h2>
- <?= Helper\markdown($task['description']) ?: t('There is no description.') ?>
+ <?= $this->text->markdown($task['description']) ?>
<?php endif ?>
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/notification/task_move_column.php b/app/Template/notification/task_move_column.php
new file mode 100644
index 00000000..88ab8ab5
--- /dev/null
+++ b/app/Template/notification/task_move_column.php
@@ -0,0 +1,11 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<ul>
+ <li>
+ <?= t('Column on the board:') ?>
+ <strong><?= $this->e($task['column_title']) ?></strong>
+ </li>
+ <li><?= t('Task position:').' '.$this->e($task['position']) ?></li>
+</ul>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/notification/task_move_position.php b/app/Template/notification/task_move_position.php
new file mode 100644
index 00000000..88ab8ab5
--- /dev/null
+++ b/app/Template/notification/task_move_position.php
@@ -0,0 +1,11 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<ul>
+ <li>
+ <?= t('Column on the board:') ?>
+ <strong><?= $this->e($task['column_title']) ?></strong>
+ </li>
+ <li><?= t('Task position:').' '.$this->e($task['position']) ?></li>
+</ul>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/notification/task_open.php b/app/Template/notification/task_open.php
new file mode 100644
index 00000000..cb02a79c
--- /dev/null
+++ b/app/Template/notification/task_open.php
@@ -0,0 +1,5 @@
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+
+<p><?= t('The task #%d have been opened.', $task['id']) ?></p>
+
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Template/notification/task_overdue.php b/app/Template/notification/task_overdue.php
new file mode 100644
index 00000000..dc2659dc
--- /dev/null
+++ b/app/Template/notification/task_overdue.php
@@ -0,0 +1,18 @@
+<h2><?= t('Overdue tasks for the project "%s"', $project_name) ?></h2>
+
+<ul>
+ <?php foreach ($tasks as $task): ?>
+ <li>
+ (<strong>#<?= $task['id'] ?></strong>)
+ <?php if ($application_url): ?>
+ <a href="<?= $application_url.$this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= $this->e($task['title']) ?></a>
+ <?php else: ?>
+ <?= $this->e($task['title']) ?>
+ <?php endif ?>
+ (<?= dt('%B %e, %Y', $task['date_due']) ?>)
+ <?php if ($task['assignee_username']): ?>
+ (<strong><?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></strong>)
+ <?php endif ?>
+ </li>
+ <?php endforeach ?>
+</ul>
diff --git a/app/Templates/notification_task_creation.php b/app/Template/notification/task_update.php
index 1b555096..ffea49cd 100644
--- a/app/Templates/notification_task_creation.php
+++ b/app/Template/notification/task_update.php
@@ -1,4 +1,4 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
+<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2>
<ul>
<li>
@@ -25,19 +25,19 @@
</li>
<li>
<?= t('Column on the board:') ?>
- <strong><?= Helper\escape($task['column_title']) ?></strong>
+ <strong><?= $this->e($task['column_title']) ?></strong>
</li>
- <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li>
+ <li><?= t('Task position:').' '.$this->e($task['position']) ?></li>
<?php if ($task['category_name']): ?>
<li>
- <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong>
+ <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong>
</li>
<?php endif ?>
</ul>
<?php if (! empty($task['description'])): ?>
<h2><?= t('Description') ?></h2>
- <?= Helper\markdown($task['description']) ?>
+ <?= $this->text->markdown($task['description']) ?: t('There is no description.') ?>
<?php endif ?>
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
+<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/project_disable.php b/app/Template/project/disable.php
index 7a729fa3..ddfcdca2 100644
--- a/app/Templates/project_disable.php
+++ b/app/Template/project/disable.php
@@ -8,7 +8,7 @@
</p>
<div class="form-actions">
- <?= Helper\a(t('Yes'), 'project', 'disable', array('project_id' => $project['id'], 'disable' => 'yes'), true, 'btn btn-red') ?>
- <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('Yes'), 'project', 'disable', array('project_id' => $project['id'], 'disable' => 'yes'), true, 'btn btn-red') ?>
+ <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/project/dropdown.php b/app/Template/project/dropdown.php
new file mode 100644
index 00000000..2e2650a7
--- /dev/null
+++ b/app/Template/project/dropdown.php
@@ -0,0 +1,41 @@
+<li>
+ <i class="fa fa-search fa-fw"></i>
+ <?= $this->url->link(t('Search'), 'projectinfo', 'search', array('project_id' => $project['id'])) ?>
+</li>
+<li>
+ <i class="fa fa-check-square-o fa-fw"></i>
+ <?= $this->url->link(t('Completed tasks'), 'projectinfo', 'tasks', array('project_id' => $project['id'])) ?>
+</li>
+<li>
+ <i class="fa fa-dashboard fa-fw"></i>
+ <?= $this->url->link(t('Activity'), 'projectinfo', 'activity', array('project_id' => $project['id'])) ?>
+</li>
+<li>
+ <i class="fa fa-calendar fa-fw"></i>
+ <?= $this->url->link(t('Calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
+</li>
+
+<?php if ($project['is_public']): ?>
+<li>
+ <i class="fa fa-share-alt fa-fw"></i> <?= $this->url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?>
+</li>
+<?php endif ?>
+
+<?php if ($this->user->isManager($project['id'])): ?>
+<li>
+ <i class="fa fa-line-chart fa-fw"></i>
+ <?= $this->url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?>
+</li>
+<li>
+ <i class="fa fa-pie-chart fa-fw"></i>
+ <?= $this->url->link(t('Budget'), 'budget', 'index', array('project_id' => $project['id'])) ?>
+</li>
+<li>
+ <i class="fa fa-download fa-fw"></i>
+ <?= $this->url->link(t('Exports'), 'export', 'tasks', array('project_id' => $project['id'])) ?>
+</li>
+<li>
+ <i class="fa fa-cog fa-fw"></i>
+ <?= $this->url->link(t('Settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
+</li>
+<?php endif ?>
diff --git a/app/Template/project/duplicate.php b/app/Template/project/duplicate.php
new file mode 100644
index 00000000..8967c306
--- /dev/null
+++ b/app/Template/project/duplicate.php
@@ -0,0 +1,23 @@
+<div class="page-header">
+ <h2><?= t('Clone this project') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Which parts of the project do you want to duplicate?') ?>
+ </p>
+ <form method="post" action="<?= $this->url->href('project', 'duplicate', array('project_id' => $project['id'], 'duplicate' => 'yes')) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $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"/>
+ <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
+ </div>
+ </form>
+</div> \ No newline at end of file
diff --git a/app/Template/project/edit.php b/app/Template/project/edit.php
new file mode 100644
index 00000000..794267f4
--- /dev/null
+++ b/app/Template/project/edit.php
@@ -0,0 +1,44 @@
+<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>
+
+ <?php if ($this->user->isAdmin()): ?>
+ <?= $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"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
diff --git a/app/Templates/project_enable.php b/app/Template/project/enable.php
index f2a1b0e7..c10d2f12 100644
--- a/app/Templates/project_enable.php
+++ b/app/Template/project/enable.php
@@ -8,7 +8,7 @@
</p>
<div class="form-actions">
- <?= Helper\a(t('Yes'), 'project', 'enable', array('project_id' => $project['id'], 'enable' => 'yes'), true, 'btn btn-red') ?>
- <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('Yes'), 'project', 'enable', array('project_id' => $project['id'], 'enable' => 'yes'), true, 'btn btn-red') ?>
+ <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Templates/project_feed.php b/app/Template/project/feed.php
index 9d10ecb1..2062e801 100644
--- a/app/Templates/project_feed.php
+++ b/app/Template/project/feed.php
@@ -1,21 +1,21 @@
<?= '<?xml version="1.0" encoding="utf-8"?>' ?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
<title><?= t('%s\'s activity', $project['name']) ?></title>
- <link rel="alternate" type="text/html" href="<?= Helper\get_current_base_url() ?>"/>
- <link rel="self" type="application/atom+xml" href="<?= Helper\get_current_base_url().Helper\u('project', 'feed', array('token' => $project['token'])) ?>"/>
+ <link rel="alternate" type="text/html" href="<?= $this->url->base() ?>"/>
+ <link rel="self" type="application/atom+xml" href="<?= $this->url->base().$this->url->href('project', 'feed', array('token' => $project['token'])) ?>"/>
<updated><?= date(DATE_ATOM) ?></updated>
- <id><?= Helper\get_current_base_url() ?></id>
- <icon><?= Helper\get_current_base_url() ?>assets/img/favicon.png</icon>
+ <id><?= $this->url->base() ?></id>
+ <icon><?= $this->url->base() ?>assets/img/favicon.png</icon>
<?php foreach ($events as $e): ?>
<entry>
<title type="text"><?= $e['event_title'] ?></title>
- <link rel="alternate" href="<?= Helper\get_current_base_url().Helper\u('task', 'show', array('task_id' => $e['task_id'])) ?>"/>
+ <link rel="alternate" href="<?= $this->url->base().$this->url->href('task', 'show', array('task_id' => $e['task_id'])) ?>"/>
<id><?= $e['id'].'-'.$e['event_name'].'-'.$e['task_id'].'-'.$e['date_creation'] ?></id>
<published><?= date(DATE_ATOM, $e['date_creation']) ?></published>
<updated><?= date(DATE_ATOM, $e['date_creation']) ?></updated>
<author>
- <name><?= Helper\escape($e['author']) ?></name>
+ <name><?= $this->e($e['author']) ?></name>
</author>
<content type="html">
<![CDATA[
diff --git a/app/Template/project/index.php b/app/Template/project/index.php
new file mode 100644
index 00000000..1080968e
--- /dev/null
+++ b/app/Template/project/index.php
@@ -0,0 +1,67 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <?php if ($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>
+ </ul>
+ </div>
+ <section>
+ <?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('No project') ?></p>
+ <?php else: ?>
+ <table class="table-fixed">
+ <tr>
+ <th class="column-8"><?= $paginator->order(t('Id'), 'id') ?></th>
+ <th class="column-8"><?= $paginator->order(t('Status'), 'is_active') ?></th>
+ <th class="column-8"><?= $paginator->order(t('Identifier'), 'identifier') ?></th>
+ <th class="column-20"><?= $paginator->order(t('Project'), 'name') ?></th>
+ <th><?= t('Columns') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $project): ?>
+ <tr>
+ <td>
+ <?= $this->url->link('#'.$project['id'], 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link') ?>
+ </td>
+ <td>
+ <?php if ($project['is_active']): ?>
+ <?= t('Active') ?>
+ <?php else: ?>
+ <?= t('Inactive') ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <?= $this->e($project['identifier']) ?>
+ </td>
+ <td>
+ <?= $this->url->link('<i class="fa fa-table"></i>', 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Board')) ?>&nbsp;
+
+ <?php if ($project['is_public']): ?>
+ <i class="fa fa-share-alt fa-fw"></i>
+ <?php endif ?>
+ <?php if ($project['is_private']): ?>
+ <i class="fa fa-lock fa-fw"></i>
+ <?php endif ?>
+
+ <?= $this->url->link($this->e($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?>
+ <?php if (! empty($project['description'])): ?>
+ <span class="column-tooltip" title='<?= $this->e($this->text->markdown($project['description'])) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?php endif ?>
+ </td>
+ <td class="dashboard-project-stats">
+ <?php foreach ($project['columns'] as $column): ?>
+ <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong>
+ <span><?= $this->e($column['title']) ?></span>
+ <?php endforeach ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+ <?php endif ?>
+ </section>
+</section>
diff --git a/app/Template/project/integrations.php b/app/Template/project/integrations.php
new file mode 100644
index 00000000..698e438c
--- /dev/null
+++ b/app/Template/project/integrations.php
@@ -0,0 +1,95 @@
+<div class="page-header">
+ <h2><?= t('Integration with third-party services') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->url->href('project', 'integration', array('project_id' => $project['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+
+ <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->base().$this->url->href('webhook', 'github', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/>
+ <p class="form-help"><a href="http://kanboard.net/documentation/github-webhooks" target="_blank"><?= t('Help on Github webhooks') ?></a></p>
+ </div>
+
+
+ <h3><img src="assets/img/gitlab-icon.png"/>&nbsp;<?= t('Gitlab webhooks') ?></h3>
+ <div class="listing">
+ <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'gitlab', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/>
+ <p class="form-help"><a href="http://kanboard.net/documentation/gitlab-webhooks" target="_blank"><?= t('Help on Gitlab webhooks') ?></a></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->base().$this->url->href('webhook', 'bitbucket', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/>
+ <p class="form-help"><a href="http://kanboard.net/documentation/bitbucket-webhooks" target="_blank"><?= t('Help on Bitbucket webhooks') ?></a></p>
+ </div>
+
+
+ <h3><img src="assets/img/jabber-icon.png"/> <?= t('Jabber (XMPP)') ?></h3>
+ <div class="listing">
+ <?= $this->form->checkbox('jabber', t('Send notifications to Jabber'), 1, isset($values['jabber']) && $values['jabber'] == 1) ?>
+
+ <?= $this->form->label(t('XMPP server address'), 'jabber_server') ?>
+ <?= $this->form->text('jabber_server', $values, $errors, array('placeholder="tcp://myserver:5222"')) ?>
+ <p class="form-help"><?= t('The server address must use this format: "tcp://hostname:5222"') ?></p>
+
+ <?= $this->form->label(t('Jabber domain'), 'jabber_domain') ?>
+ <?= $this->form->text('jabber_domain', $values, $errors, array('placeholder="example.com"')) ?>
+
+ <?= $this->form->label(t('Username'), 'jabber_username') ?>
+ <?= $this->form->text('jabber_username', $values, $errors) ?>
+
+ <?= $this->form->label(t('Password'), 'jabber_password') ?>
+ <?= $this->form->password('jabber_password', $values, $errors) ?>
+
+ <?= $this->form->label(t('Jabber nickname'), 'jabber_nickname') ?>
+ <?= $this->form->text('jabber_nickname', $values, $errors) ?>
+
+ <?= $this->form->label(t('Multi-user chat room'), 'jabber_room') ?>
+ <?= $this->form->text('jabber_room', $values, $errors, array('placeholder="myroom@conference.example.com"')) ?>
+
+ <p class="form-help"><a href="http://kanboard.net/documentation/jabber" target="_blank"><?= t('Help on Jabber integration') ?></a></p>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+ </div>
+
+
+ <h3><img src="assets/img/hipchat-icon.png"/> <?= t('Hipchat') ?></h3>
+ <div class="listing">
+ <?= $this->form->checkbox('hipchat', t('Send notifications to Hipchat'), 1, isset($values['hipchat']) && $values['hipchat'] == 1) ?>
+
+ <?= $this->form->label(t('API URL'), 'hipchat_api_url') ?>
+ <?= $this->form->text('hipchat_api_url', $values, $errors) ?>
+
+ <?= $this->form->label(t('Room API ID or name'), 'hipchat_room_id') ?>
+ <?= $this->form->text('hipchat_room_id', $values, $errors) ?>
+
+ <?= $this->form->label(t('Room notification token'), 'hipchat_room_token') ?>
+ <?= $this->form->text('hipchat_room_token', $values, $errors) ?>
+
+ <p class="form-help"><a href="http://kanboard.net/documentation/hipchat" target="_blank"><?= t('Help on Hipchat integration') ?></a></p>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+ </div>
+
+
+ <h3><i class="fa fa-slack fa-fw"></i>&nbsp;<?= t('Slack') ?></h3>
+ <div class="listing">
+ <?= $this->form->checkbox('slack', t('Send notifications to a Slack channel'), 1, isset($values['slack']) && $values['slack'] == 1) ?>
+
+ <?= $this->form->label(t('Webhook URL'), 'slack_webhook_url') ?>
+ <?= $this->form->text('slack_webhook_url', $values, $errors) ?>
+
+ <p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/project/layout.php b/app/Template/project/layout.php
new file mode 100644
index 00000000..7bb3d478
--- /dev/null
+++ b/app/Template/project/layout.php
@@ -0,0 +1,32 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <span class="dropdown">
+ <span>
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <ul>
+ <?= $this->render('project/dropdown', array('project' => $project)) ?>
+ </ul>
+ </span>
+ </span>
+ </li>
+ <li>
+ <i class="fa fa-table fa-fw"></i>
+ <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-folder fa-fw"></i>
+ <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ </li>
+ </ul>
+ </div>
+ <section class="sidebar-container">
+
+ <?= $this->render($sidebar_template, array('project' => $project)) ?>
+
+ <div class="sidebar-content">
+ <?= $project_content_for_layout ?>
+ </div>
+ </section>
+</section> \ No newline at end of file
diff --git a/app/Template/project/new.php b/app/Template/project/new.php
new file mode 100644
index 00000000..8e4ccfec
--- /dev/null
+++ b/app/Template/project/new.php
@@ -0,0 +1,24 @@
+<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/Templates/project_remove.php b/app/Template/project/remove.php
index a98f94eb..fa43fc78 100644
--- a/app/Templates/project_remove.php
+++ b/app/Template/project/remove.php
@@ -8,7 +8,7 @@
</p>
<div class="form-actions">
- <?= Helper\a(t('Yes'), 'project', 'remove', array('project_id' => $project['id'], 'remove' => 'yes'), true, 'btn btn-red') ?>
- <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
+ <?= $this->url->link(t('Yes'), 'project', 'remove', array('project_id' => $project['id'], 'remove' => 'yes'), true, 'btn btn-red') ?>
+ <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
</div>
</div> \ No newline at end of file
diff --git a/app/Template/project/share.php b/app/Template/project/share.php
new file mode 100644
index 00000000..a9146599
--- /dev/null
+++ b/app/Template/project/share.php
@@ -0,0 +1,19 @@
+<div class="page-header">
+ <h2><?= t('Public access') ?></h2>
+</div>
+
+<?php if ($project['is_public']): ?>
+
+ <div class="listing">
+ <ul class="no-bullet">
+ <li><strong><i class="fa fa-share-alt"></i> <?= $this->url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?></strong></li>
+ <li><strong><i class="fa fa-rss-square"></i> <?= $this->url->link(t('RSS feed'), 'project', 'feed', array('token' => $project['token']), false, '', '', true) ?></strong></li>
+ <li><strong><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token']), false, '', '', true) ?></strong></li>
+ </ul>
+ </div>
+
+ <?= $this->url->link(t('Disable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'disable'), true, 'btn btn-red') ?>
+
+<?php else: ?>
+ <?= $this->url->link(t('Enable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?>
+<?php endif ?>
diff --git a/app/Template/project/show.php b/app/Template/project/show.php
new file mode 100644
index 00000000..4869d8a4
--- /dev/null
+++ b/app/Template/project/show.php
@@ -0,0 +1,73 @@
+<div class="page-header">
+ <h2><?= t('Summary') ?></h2>
+</div>
+<ul class="listing">
+ <li><strong><?= $project['is_active'] ? t('Active') : t('Inactive') ?></strong></li>
+
+ <?php if ($project['is_private']): ?>
+ <li><i class="fa fa-lock"></i> <?= t('This project is private') ?></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'), 'project', 'feed', 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 else: ?>
+ <li><?= t('Public access disabled') ?></li>
+ <?php endif ?>
+
+ <?php if ($project['last_modified']): ?>
+ <li><?= dt('Last modified on %B %e, %Y at %k:%M %p', $project['last_modified']) ?></li>
+ <?php endif ?>
+
+ <?php if ($stats['nb_tasks'] > 0): ?>
+
+ <?php if ($stats['nb_active_tasks'] > 0): ?>
+ <li><?= $this->url->link(t('%d tasks on the board', $stats['nb_active_tasks']), 'board', 'show', array('project_id' => $project['id'])) ?></li>
+ <?php endif ?>
+
+ <?php if ($stats['nb_inactive_tasks'] > 0): ?>
+ <li><?= $this->url->link(t('%d closed tasks', $stats['nb_inactive_tasks']), 'project', 'tasks', array('project_id' => $project['id'])) ?></li>
+ <?php endif ?>
+
+ <li><?= t('%d tasks in total', $stats['nb_tasks']) ?></li>
+
+ <?php else: ?>
+ <li><?= t('No task for this project') ?></li>
+ <?php endif ?>
+</ul>
+
+<div class="page-header">
+ <h2><?= t('Board') ?></h2>
+</div>
+<table class="table-stripped">
+ <tr>
+ <th class="column-60"><?= t('Column') ?></th>
+ <th class="column-20"><?= t('Task limit') ?></th>
+ <th class="column-20"><?= t('Active tasks') ?></th>
+ </tr>
+ <?php foreach ($stats['columns'] as $column): ?>
+ <tr>
+ <td>
+ <?= $this->e($column['title']) ?>
+ <?php if (! empty($column['description'])): ?>
+ <span class="column-tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'>
+ <i class="fa fa-info-circle"></i>
+ </span>
+ <?php endif ?>
+ </td>
+ <td><?= $column['task_limit'] ?: '∞' ?></td>
+ <td><?= $column['nb_active_tasks'] ?></td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<?php if (! empty($project['description'])): ?>
+ <div class="page-header">
+ <h2><?= t('Description') ?></h2>
+ </div>
+
+ <article class="markdown">
+ <?= $this->text->markdown($project['description']) ?>
+ </article>
+<?php endif ?>
diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php
new file mode 100644
index 00000000..a58c4604
--- /dev/null
+++ b/app/Template/project/sidebar.php
@@ -0,0 +1,52 @@
+<div class="sidebar">
+ <h2><?= t('Actions') ?></h2>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Summary'), 'project', 'show', array('project_id' => $project['id'])) ?>
+ </li>
+
+ <?php if ($this->user->isManager($project['id'])): ?>
+ <li>
+ <?= $this->url->link(t('Public access'), 'project', 'share', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Integrations'), 'project', 'integration', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Edit project'), 'project', 'edit', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Edit board'), 'column', 'index', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Swimlanes'), 'swimlane', 'index', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Categories'), 'category', 'index', array('project_id' => $project['id'])) ?>
+ </li>
+ <?php if ($this->user->isAdmin() || $project['is_private'] == 0): ?>
+ <li>
+ <?= $this->url->link(t('Users'), 'project', 'users', array('project_id' => $project['id'])) ?>
+ </li>
+ <?php endif ?>
+ <li>
+ <?= $this->url->link(t('Automatic actions'), 'action', 'index', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Duplicate'), 'project', 'duplicate', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <?php if ($project['is_active']): ?>
+ <?= $this->url->link(t('Disable'), 'project', 'disable', array('project_id' => $project['id']), true) ?>
+ <?php else: ?>
+ <?= $this->url->link(t('Enable'), 'project', 'enable', array('project_id' => $project['id']), true) ?>
+ <?php endif ?>
+ </li>
+ <?php if ($this->user->isAdmin()): ?>
+ <li>
+ <?= $this->url->link(t('Remove'), 'project', 'remove', array('project_id' => $project['id'])) ?>
+ </li>
+ <?php endif ?>
+ <?php endif ?>
+ </ul>
+</div>
diff --git a/app/Template/project/users.php b/app/Template/project/users.php
new file mode 100644
index 00000000..d725a9e8
--- /dev/null
+++ b/app/Template/project/users.php
@@ -0,0 +1,81 @@
+<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>
+ </ul>
+</div>
diff --git a/app/Template/projectinfo/activity.php b/app/Template/projectinfo/activity.php
new file mode 100644
index 00000000..528cdbee
--- /dev/null
+++ b/app/Template/projectinfo/activity.php
@@ -0,0 +1,30 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <span class="dropdown">
+ <span>
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <ul>
+ <?= $this->render('project/dropdown', array('project' => $project)) ?>
+ </ul>
+ </span>
+ </span>
+ </li>
+ <li>
+ <i class="fa fa-table fa-fw"></i>
+ <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-folder fa-fw"></i>
+ <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ </li>
+ <?php if ($project['is_public']): ?>
+ <li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'project', 'feed', array('token' => $project['token']), false, '', '', true) ?></li>
+ <li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li>
+ <?php endif ?>
+ </ul>
+ </div>
+
+ <?= $this->render('event/events', array('events' => $events)) ?>
+</section> \ No newline at end of file
diff --git a/app/Template/projectinfo/search.php b/app/Template/projectinfo/search.php
new file mode 100644
index 00000000..4b7c8f70
--- /dev/null
+++ b/app/Template/projectinfo/search.php
@@ -0,0 +1,43 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <span class="dropdown">
+ <span>
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <ul>
+ <?= $this->render('project/dropdown', array('project' => $project)) ?>
+ </ul>
+ </span>
+ </span>
+ </li>
+ <li>
+ <i class="fa fa-table fa-fw"></i>
+ <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-folder fa-fw"></i>
+ <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ </li>
+ </ul>
+ </div>
+
+ <form method="get" action="?" autocomplete="off">
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $this->form->hidden('project_id', $values) ?>
+ <?= $this->form->text('search', $values, array(), array('autofocus', 'required', 'placeholder="'.t('Search').'"'), 'form-input-large') ?>
+ <input type="submit" value="<?= t('Search') ?>" class="btn btn-blue"/>
+ </form>
+
+ <?php if (! empty($values['search']) && $paginator->isEmpty()): ?>
+ <p class="alert"><?= t('Nothing found.') ?></p>
+ <?php elseif (! $paginator->isEmpty()): ?>
+ <?= $this->render('task/table', array(
+ 'paginator' => $paginator,
+ 'categories' => $categories,
+ 'columns' => $columns,
+ )) ?>
+ <?php endif ?>
+
+</section> \ No newline at end of file
diff --git a/app/Template/projectinfo/tasks.php b/app/Template/projectinfo/tasks.php
new file mode 100644
index 00000000..41884783
--- /dev/null
+++ b/app/Template/projectinfo/tasks.php
@@ -0,0 +1,33 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <span class="dropdown">
+ <span>
+ <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <ul>
+ <?= $this->render('project/dropdown', array('project' => $project)) ?>
+ </ul>
+ </span>
+ </span>
+ </li>
+ <li>
+ <i class="fa fa-table fa-fw"></i>
+ <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
+ </li>
+ <li>
+ <i class="fa fa-folder fa-fw"></i>
+ <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ </li>
+ </ul>
+ </div>
+ <?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is no completed tasks at the moment.') ?></p>
+ <?php else: ?>
+ <?= $this->render('task/table', array(
+ 'paginator' => $paginator,
+ 'categories' => $categories,
+ 'columns' => $columns,
+ )) ?>
+ <?php endif ?>
+</section> \ No newline at end of file
diff --git a/app/Template/subtask/create.php b/app/Template/subtask/create.php
new file mode 100644
index 00000000..82e378f5
--- /dev/null
+++ b/app/Template/subtask/create.php
@@ -0,0 +1,27 @@
+<div class="page-header">
+ <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">
+
+ <?= $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->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"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </div>
+</form>
diff --git a/app/Template/subtask/edit.php b/app/Template/subtask/edit.php
new file mode 100644
index 00000000..2e583069
--- /dev/null
+++ b/app/Template/subtask/edit.php
@@ -0,0 +1,29 @@
+<div class="page-header">
+ <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">
+
+ <?= $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/>
+
+ <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>
diff --git a/app/Template/subtask/icons.php b/app/Template/subtask/icons.php
new file mode 100644
index 00000000..1f31d51f
--- /dev/null
+++ b/app/Template/subtask/icons.php
@@ -0,0 +1,7 @@
+<?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/remove.php b/app/Template/subtask/remove.php
new file mode 100644
index 00000000..65ade31d
--- /dev/null
+++ b/app/Template/subtask/remove.php
@@ -0,0 +1,17 @@
+<div class="page-header">
+ <h2><?= t('Remove a sub-task') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this sub-task?') ?>
+ </p>
+
+ <p><strong><?= $this->e($subtask['title']) ?></strong></p>
+
+ <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'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/subtask/restriction_change_status.php b/app/Template/subtask/restriction_change_status.php
new file mode 100644
index 00000000..88e91d82
--- /dev/null
+++ b/app/Template/subtask/restriction_change_status.php
@@ -0,0 +1,19 @@
+<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">
+
+ <?= $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"/>
+ <?= t('or') ?>
+ <a href="#" class="close-popover"><?= t('cancel') ?></a>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/subtask/show.php b/app/Template/subtask/show.php
new file mode 100644
index 00000000..c9690f08
--- /dev/null
+++ b/app/Template/subtask/show.php
@@ -0,0 +1,80 @@
+<?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>
+
+ <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'])): ?>
+ <?= $this->url->link($this->e($subtask['name'] ?: $subtask['username']), 'user', 'show', array('user_id' => $subtask['user_id'])) ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <?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 ?>
+ </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>
+
+ <?php if (! isset($not_editable)): ?>
+ <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'])) ?>
+ <?= $this->form->text('title', array(), array(), array('required', 'placeholder="'.t('Type here to create a new sub-task').'"')) ?>
+ <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
+ </form>
+ <?php endif ?>
+
+</div>
+<?php endif ?>
diff --git a/app/Template/swimlane/edit.php b/app/Template/swimlane/edit.php
new file mode 100644
index 00000000..cc98b584
--- /dev/null
+++ b/app/Template/swimlane/edit.php
@@ -0,0 +1,18 @@
+<div class="page-header">
+ <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">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('project_id', $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"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/swimlane/index.php b/app/Template/swimlane/index.php
new file mode 100644
index 00000000..daee6af5
--- /dev/null
+++ b/app/Template/swimlane/index.php
@@ -0,0 +1,47 @@
+<?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 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('autofocus', 'required', 'maxlength="50"')) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<div class="page-header">
+ <h2><?= t('Change default swimlane') ?></h2>
+</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('autofocus', 'required', 'maxlength="50"')) ?><br/>
+
+ <?= $this->form->checkbox('show_default_swimlane', t('Show default swimlane'), 1, isset($default_swimlane['show_default_swimlane']) && $default_swimlane['show_default_swimlane'] == 1) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<?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 endif ?> \ No newline at end of file
diff --git a/app/Template/swimlane/remove.php b/app/Template/swimlane/remove.php
new file mode 100644
index 00000000..1d7c2b7a
--- /dev/null
+++ b/app/Template/swimlane/remove.php
@@ -0,0 +1,17 @@
+<section id="main">
+ <div class="page-header">
+ <h2><?= t('Remove a swimlane') ?></h2>
+ </div>
+
+ <div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this swimlane: "%s"?', $swimlane['name']) ?>
+ </p>
+
+ <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'])) ?>
+ </div>
+ </div>
+</section> \ No newline at end of file
diff --git a/app/Template/swimlane/table.php b/app/Template/swimlane/table.php
new file mode 100644
index 00000000..f38572a3
--- /dev/null
+++ b/app/Template/swimlane/table.php
@@ -0,0 +1,44 @@
+<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): ?>
+ <li>
+ <?= $this->url->link(t('Move Up'), 'swimlane', 'moveup', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?>
+ </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) ?>
+ </li>
+ <?php endif ?>
+ <li>
+ <?= $this->url->link(t('Rename'), '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
diff --git a/app/Template/task/activity.php b/app/Template/task/activity.php
new file mode 100644
index 00000000..cc4aad03
--- /dev/null
+++ b/app/Template/task/activity.php
@@ -0,0 +1,5 @@
+<div class="page-header">
+ <h2><?= t('Activity stream') ?></h2>
+</div>
+
+<?= $this->render('event/events', array('events' => $events)) ?> \ No newline at end of file
diff --git a/app/Template/task/close.php b/app/Template/task/close.php
new file mode 100644
index 00000000..79150333
--- /dev/null
+++ b/app/Template/task/close.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Close a task') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to close this task: "%s"?', $this->e($task['title'])) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $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/Templates/task_comments.php b/app/Template/task/comments.php
index 5cfa99ce..a09862f9 100644
--- a/app/Templates/task_comments.php
+++ b/app/Template/task/comments.php
@@ -5,7 +5,7 @@
</div>
<?php foreach ($comments as $comment): ?>
- <?= Helper\template('comment_show', array(
+ <?= $this->render('comment/show', array(
'comment' => $comment,
'task' => $task,
'project' => $project,
@@ -15,10 +15,10 @@
<?php endforeach ?>
<?php if (! isset($not_editable)): ?>
- <?= Helper\template('comment_create', array(
+ <?= $this->render('comment/create', array(
'skip_cancel' => true,
'values' => array(
- 'user_id' => Helper\get_user_id(),
+ 'user_id' => $this->user->getId(),
'task_id' => $task['id'],
),
'errors' => array(),
diff --git a/app/Templates/task_details.php b/app/Template/task/details.php
index a4fdf6ce..f688585a 100644
--- a/app/Templates/task_details.php
+++ b/app/Template/task/details.php
@@ -1,7 +1,7 @@
-<div class="task-<?= $task['color_id'] ?> task-show-details">
- <h2><?= Helper\escape($task['title']) ?></h2>
+<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"><?= Helper\escape($task['score']) ?></span>
+ <span class="task-score"><?= $this->e($task['score']) ?></span>
<?php endif ?>
<ul>
<?php if ($task['reference']): ?>
@@ -58,13 +58,14 @@
</li>
<li>
<?= t('Column on the board:') ?>
- <strong><?= Helper\escape($task['column_title']) ?></strong>
- (<?= Helper\escape($task['project_name']) ?>)
+ <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:').' '.Helper\escape($task['position']) ?></li>
+ <li><?= t('Task position:').' '.$this->e($task['position']) ?></li>
<?php if ($task['category_name']): ?>
<li>
- <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong>
+ <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong>
</li>
<?php endif ?>
<li>
@@ -76,7 +77,19 @@
</li>
<?php if ($project['is_public']): ?>
<li>
- <a href="?controller=task&amp;action=readonly&amp;task_id=<?= $task['id'] ?>&amp;token=<?= $project['token'] ?>" target="_blank"><?= t('Public link') ?></a>
+ <?= $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'] != \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>
diff --git a/app/Template/task/duplicate.php b/app/Template/task/duplicate.php
new file mode 100644
index 00000000..e74d2906
--- /dev/null
+++ b/app/Template/task/duplicate.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Duplicate a task') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to duplicate this task?') ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'task', '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'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/task/duplicate_project.php b/app/Template/task/duplicate_project.php
new file mode 100644
index 00000000..9a8e3c4a
--- /dev/null
+++ b/app/Template/task/duplicate_project.php
@@ -0,0 +1,24 @@
+<div class="page-header">
+ <h2><?= t('Duplicate the task to another project') ?></h2>
+</div>
+
+<?php if (empty($projects_list)): ?>
+ <p class="alert"><?= t('No project') ?></p>
+<?php else: ?>
+
+ <form method="post" action="<?= $this->url->href('task', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->label(t('Project'), 'project_id') ?>
+ <?= $this->form->select('project_id', $projects_list, $values, $errors) ?><br/>
+
+ <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>
+
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/task/edit.php b/app/Template/task/edit.php
new file mode 100644
index 00000000..2900b739
--- /dev/null
+++ b/app/Template/task/edit.php
@@ -0,0 +1,68 @@
+<div class="page-header">
+ <h2><?= t('Edit a task') ?></h2>
+</div>
+<section id="task-section">
+<form method="post" action="<?= $this->url->href('task', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <div class="form-column">
+
+ <?= $this->form->label(t('Title'), 'title') ?>
+ <?= $this->form->text('title', $values, $errors, array('required', 'maxlength="200"')) ?><br/>
+
+ <?= $this->form->label(t('Description'), 'description') ?>
+
+ <div class="form-tabs">
+ <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 class="write-area">
+ <?= $this->form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"')) ?>
+ </div>
+ <div class="preview-area">
+ <div class="markdown"></div>
+ </div>
+ </div>
+
+ <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
+
+ </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) ?><br/>
+
+ <?= $this->form->label(t('Category'), 'category_id') ?>
+ <?= $this->form->select('category_id', $categories_list, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Color'), 'color_id') ?>
+ <?= $this->form->select('color_id', $colors_list, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Complexity'), 'score') ?>
+ <?= $this->form->number('score', $values, $errors) ?><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).'"'), 'form-date') ?><br/>
+ <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
+ </div>
+
+ <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']), false, 'close-popover') ?>
+ <?php else: ?>
+ <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?php endif ?>
+ </div>
+</form>
+</section>
diff --git a/app/Template/task/edit_description.php b/app/Template/task/edit_description.php
new file mode 100644
index 00000000..84f0cebd
--- /dev/null
+++ b/app/Template/task/edit_description.php
@@ -0,0 +1,38 @@
+<div class="page-header">
+ <h2><?= t('Edit the description') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->url->href('task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+
+ <div class="form-tabs">
+ <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 class="write-area">
+ <?= $this->form->textarea('description', $values, $errors, array('autofocus', 'placeholder="'.t('Leave a description').'"'), 'description-textarea') ?>
+ </div>
+ <div class="preview-area">
+ <div class="markdown"></div>
+ </div>
+ </div>
+
+ <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
+
+ <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 ?>
+ </div>
+</form>
diff --git a/app/Template/task/edit_recurrence.php b/app/Template/task/edit_recurrence.php
new file mode 100644
index 00000000..c261e368
--- /dev/null
+++ b/app/Template/task/edit_recurrence.php
@@ -0,0 +1,52 @@
+<div class="page-header">
+ <h2><?= t('Edit recurrence') ?></h2>
+</div>
+
+<?php if ($task['recurrence_status'] != \Model\Task::RECURRING_STATUS_NONE): ?>
+<div class="listing">
+ <?= $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,
+ )) ?>
+</div>
+<?php endif ?>
+
+<?php if ($task['recurrence_status'] != \Model\Task::RECURRING_STATUS_PROCESSED): ?>
+
+ <form method="post" action="<?= $this->url->href('task', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('project_id', $values) ?>
+
+ <?= $this->form->label(t('Generate recurrent task'), 'recurrence_status') ?>
+ <?= $this->form->select('recurrence_status', $recurrence_status_list, $values, $errors) ?>
+
+ <?= $this->form->label(t('Trigger to generate recurrent task'), 'recurrence_trigger') ?>
+ <?= $this->form->select('recurrence_trigger', $recurrence_trigger_list, $values, $errors) ?>
+
+ <?= $this->form->label(t('Factor to calculate new due date'), 'recurrence_factor') ?>
+ <?= $this->form->number('recurrence_factor', $values, $errors) ?>
+
+ <?= $this->form->label(t('Timeframe to calculate new due date'), 'recurrence_timeframe') ?>
+ <?= $this->form->select('recurrence_timeframe', $recurrence_timeframe_list, $values, $errors) ?>
+
+ <?= $this->form->label(t('Base date to calculate new due date'), 'recurrence_basedate') ?>
+ <?= $this->form->select('recurrence_basedate', $recurrence_basedate_list, $values, $errors) ?>
+
+ <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 ?>
+ </div>
+ </form>
+
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php
new file mode 100644
index 00000000..5a14fb39
--- /dev/null
+++ b/app/Template/task/layout.php
@@ -0,0 +1,28 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <i class="fa fa-table fa-fw"></i>
+ <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, 'swimlane-'.$task['swimlane_id']) ?>
+ </li>
+ <?php if ($this->user->isManager($task['project_id'])): ?>
+ <li>
+ <i class="fa fa-cog fa-fw"></i>
+ <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $task['project_id'])) ?>
+ </li>
+ <?php endif ?>
+ <li>
+ <i class="fa fa-calendar fa-fw"></i>
+ <?= $this->url->link(t('Project calendar'), 'calendar', 'show', array('project_id' => $task['project_id'])) ?>
+ </li>
+ </ul>
+ </div>
+ <section class="sidebar-container" id="task-section">
+
+ <?= $this->render('task/sidebar', array('task' => $task)) ?>
+
+ <div class="sidebar-content">
+ <?= $task_content_for_layout ?>
+ </div>
+ </section>
+</section> \ No newline at end of file
diff --git a/app/Template/task/move_project.php b/app/Template/task/move_project.php
new file mode 100644
index 00000000..b0b33f81
--- /dev/null
+++ b/app/Template/task/move_project.php
@@ -0,0 +1,24 @@
+<div class="page-header">
+ <h2><?= t('Move the task to another project') ?></h2>
+</div>
+
+<?php if (empty($projects_list)): ?>
+ <p class="alert"><?= t('No project') ?></p>
+<?php else: ?>
+
+ <form method="post" action="<?= $this->url->href('task', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->label(t('Project'), 'project_id') ?>
+ <?= $this->form->select('project_id', $projects_list, $values, $errors) ?><br/>
+
+ <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>
+
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/task/new.php b/app/Template/task/new.php
new file mode 100644
index 00000000..bd00d347
--- /dev/null
+++ b/app/Template/task/new.php
@@ -0,0 +1,84 @@
+<?php if (! $ajax): ?>
+<div class="page-header">
+ <ul>
+ <li><i class="fa fa-table 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 ?>
+
+<section id="task-section">
+<form method="post" action="<?= $this->url->href('task', '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"'), 'form-input-large') ?><br/>
+
+ <?= $this->form->label(t('Description'), 'description') ?>
+
+ <div class="form-tabs">
+ <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 class="write-area">
+ <?= $this->form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"')) ?>
+ </div>
+ <div class="preview-area">
+ <div class="markdown"></div>
+ </div>
+ </div>
+
+ <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
+
+ <?php if (! isset($duplicate)): ?>
+ <?= $this->form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?>
+ <?php endif ?>
+ </div>
+
+ <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) ?><br/>
+
+ <?= $this->form->label(t('Category'), 'category_id') ?>
+ <?= $this->form->select('category_id', $categories_list, $values, $errors) ?><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) ?><br/>
+ <?php endif ?>
+
+ <?= $this->form->label(t('Column'), 'column_id') ?>
+ <?= $this->form->select('column_id', $columns_list, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Color'), 'color_id') ?>
+ <?= $this->form->select('color_id', $colors_list, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Complexity'), 'score') ?>
+ <?= $this->form->number('score', $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('Due Date'), 'date_due') ?>
+ <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/>
+ <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
+ </div>
+
+ <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' => $values['project_id']), false, 'close-popover') ?>
+ </div>
+</form>
+</section>
diff --git a/app/Template/task/open.php b/app/Template/task/open.php
new file mode 100644
index 00000000..fbcc1111
--- /dev/null
+++ b/app/Template/task/open.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Open a task') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to open this task: "%s"?', $this->e($task['title'])) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'task', 'open', 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'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/task/public.php b/app/Template/task/public.php
new file mode 100644
index 00000000..73116de9
--- /dev/null
+++ b/app/Template/task/public.php
@@ -0,0 +1,34 @@
+<section id="main" class="public-task">
+
+ <?= $this->render('task/details', array('task' => $task, 'project' => $project, 'not_editable' => true)) ?>
+
+ <p class="pull-right"><?= $this->url->link(t('Back to the board'), 'board', 'readonly', array('token' => $project['token'])) ?></p>
+
+ <?= $this->render('task/show_description', array(
+ 'task' => $task,
+ 'project' => $project,
+ 'is_public' => true
+ )) ?>
+
+ <?= $this->render('tasklink/show', array(
+ 'task' => $task,
+ 'links' => $links,
+ 'project' => $project,
+ 'not_editable' => true
+ )) ?>
+
+ <?= $this->render('subtask/show', array(
+ 'task' => $task,
+ 'subtasks' => $subtasks,
+ 'not_editable' => true
+ )) ?>
+
+ <?= $this->render('task/comments', array(
+ 'task' => $task,
+ 'comments' => $comments,
+ 'project' => $project,
+ 'not_editable' => true,
+ 'is_public' => true,
+ )) ?>
+
+</section> \ No newline at end of file
diff --git a/app/Template/task/recurring_info.php b/app/Template/task/recurring_info.php
new file mode 100644
index 00000000..ad64ae19
--- /dev/null
+++ b/app/Template/task/recurring_info.php
@@ -0,0 +1,37 @@
+<ul>
+ <?php if ($task['recurrence_status'] == \Model\Task::RECURRING_STATUS_PENDING): ?>
+ <li><?= t('Recurrent task is scheduled to be generated') ?></li>
+ <?php elseif ($task['recurrence_status'] == \Model\Task::RECURRING_STATUS_PROCESSED): ?>
+ <li><?= t('Recurrent task has been generated:') ?>
+ <ul>
+ <li>
+ <?= t('Trigger to generate recurrent task: ') ?><strong><?= $this->e($recurrence_trigger_list[$task['recurrence_trigger']]) ?></strong>
+ </li>
+ <li>
+ <?= t('Factor to calculate new due date: ') ?><strong><?= $this->e($task['recurrence_factor']) ?></strong>
+ </li>
+ <li>
+ <?= t('Timeframe to calculate new due date: ') ?><strong><?= $this->e($recurrence_timeframe_list[$task['recurrence_timeframe']]) ?></strong>
+ </li>
+ <li>
+ <?= t('Base date to calculate new due date: ') ?><strong><?= $this->e($recurrence_basedate_list[$task['recurrence_basedate']]) ?></strong>
+ </li>
+ </ul>
+ </li>
+ <?php endif ?>
+
+ <?php if ($task['recurrence_parent'] || $task['recurrence_child']): ?>
+ <?php if ($task['recurrence_parent']): ?>
+ <li>
+ <?= t('This task has been created by: ') ?>
+ <?= $this->url->link('#'.$task['recurrence_parent'], 'task', 'show', array('task_id' => $task['recurrence_parent'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <?php endif ?>
+ <?php if ($task['recurrence_child']): ?>
+ <li>
+ <?= t('This task has created this child task: ') ?>
+ <?= $this->url->link('#'.$task['recurrence_child'], 'task', 'show', array('task_id' => $task['recurrence_child'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <?php endif ?>
+ <?php endif ?>
+</ul> \ No newline at end of file
diff --git a/app/Template/task/remove.php b/app/Template/task/remove.php
new file mode 100644
index 00000000..2f6edc22
--- /dev/null
+++ b/app/Template/task/remove.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Remove a task') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this task: "%s"?', $this->e($task['title'])) ?>
+ </p>
+
+ <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'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/task/show.php b/app/Template/task/show.php
new file mode 100644
index 00000000..54c124f6
--- /dev/null
+++ b/app/Template/task/show.php
@@ -0,0 +1,15 @@
+<?= $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(),
+)) ?>
+
+<?= $this->render('task/time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?>
+<?= $this->render('task/show_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)) ?>
+<?= $this->render('task/timesheet', 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)) ?>
diff --git a/app/Templates/task_show_description.php b/app/Template/task/show_description.php
index 25312149..f823e7d6 100644
--- a/app/Templates/task_show_description.php
+++ b/app/Template/task/show_description.php
@@ -6,9 +6,18 @@
<article class="markdown task-show-description">
<?php if (! isset($is_public)): ?>
- <?= Helper\markdown($task['description']) ?>
+ <?= $this->text->markdown(
+ $task['description'],
+ array(
+ 'controller' => 'task',
+ 'action' => 'show',
+ 'params' => array(
+ 'project_id' => $task['project_id']
+ )
+ )
+ ) ?>
<?php else: ?>
- <?= Helper\markdown(
+ <?= $this->text->markdown(
$task['description'],
array(
'controller' => 'task',
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
new file mode 100644
index 00000000..bb137ac9
--- /dev/null
+++ b/app/Template/task/sidebar.php
@@ -0,0 +1,67 @@
+<div class="sidebar">
+ <h2><?= t('Information') ?></h2>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Summary'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Activity stream'), 'task', 'activites', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Transitions'), 'task', 'transitions', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?>
+ <li>
+ <?= $this->url->link(t('Time tracking'), 'task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ <h2><?= t('Actions') ?></h2>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Edit the task'), 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Edit the description'), 'task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Edit recurrence'), 'task', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Attach a document'), 'file', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Add a screenshot'), 'file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Duplicate'), 'task', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Duplicate to another project'), 'task', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Move to another project'), 'task', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <li>
+ <?php if ($task['is_active'] == 1): ?>
+ <?= $this->url->link(t('Close this task'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?php else: ?>
+ <?= $this->url->link(t('Open this task'), 'task', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ <?php endif ?>
+ </li>
+ <?php if ($this->task->canRemove($task)): ?>
+ <li>
+ <?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </li>
+ <?php endif ?>
+ </ul>
+</div>
diff --git a/app/Template/task/table.php b/app/Template/task/table.php
new file mode 100644
index 00000000..d06bc7b7
--- /dev/null
+++ b/app/Template/task/table.php
@@ -0,0 +1,56 @@
+<table class="table-fixed table-small">
+ <tr>
+ <th class="column-8"><?= $paginator->order(t('Id'), 'tasks.id') ?></th>
+ <th class="column-8"><?= $paginator->order(t('Column'), 'tasks.column_id') ?></th>
+ <th class="column-8"><?= $paginator->order(t('Category'), 'tasks.category_id') ?></th>
+ <th><?= $paginator->order(t('Title'), 'tasks.title') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Assignee'), 'users.username') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Due date'), 'tasks.date_due') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Date created'), 'tasks.date_creation') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Date completed'), 'tasks.date_completed') ?></th>
+ <th class="column-5"><?= $paginator->order(t('Status'), 'tasks.is_active') ?></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->text->in($task['column_id'], $columns) ?>
+ </td>
+ <td>
+ <?= $this->text->in($task['category_id'], $categories, '') ?>
+ </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['assignee_username']): ?>
+ <?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>
+ <?php else: ?>
+ <?= t('Unassigned') ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <?= dt('%B %e, %Y', $task['date_due']) ?>
+ </td>
+ <td>
+ <?= dt('%B %e, %Y', $task['date_creation']) ?>
+ </td>
+ <td>
+ <?php if ($task['date_completed']): ?>
+ <?= dt('%B %e, %Y', $task['date_completed']) ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <?php if ($task['is_active'] == \Model\Task::STATUS_OPEN): ?>
+ <?= t('Open') ?>
+ <?php else: ?>
+ <?= t('Closed') ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<?= $paginator ?>
diff --git a/app/Template/task/time.php b/app/Template/task/time.php
new file mode 100644
index 00000000..6682a08d
--- /dev/null
+++ b/app/Template/task/time.php
@@ -0,0 +1,15 @@
+<form method="post" action="<?= $this->url->href('task', 'time', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" class="form-inline task-time-form" autocomplete="off">
+ <?= $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-date') ?>
+
+ <?= $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/time_tracking.php b/app/Template/task/time_tracking.php
new file mode 100644
index 00000000..441cb585
--- /dev/null
+++ b/app/Template/task/time_tracking.php
@@ -0,0 +1,27 @@
+<?= $this->render('task/timesheet', array('task' => $task)) ?>
+
+<h3><?= t('Subtask timesheet') ?></h3>
+<?php if ($subtask_paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is nothing to show.') ?></p>
+<?php else: ?>
+ <table class="table-fixed">
+ <tr>
+ <th class="column-15"><?= $subtask_paginator->order(t('User'), 'username') ?></th>
+ <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>
+ </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><?= n($record['time_spent']).' '.t('hours') ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $subtask_paginator ?>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/task/timesheet.php b/app/Template/task/timesheet.php
new file mode 100644
index 00000000..0210be7e
--- /dev/null
+++ b/app/Template/task/timesheet.php
@@ -0,0 +1,13 @@
+<?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?>
+
+<div class="page-header">
+ <h2><?= t('Time tracking') ?></h2>
+</div>
+
+<ul class="listing">
+ <li><?= t('Estimate:') ?> <strong><?= $this->e($task['time_estimated']) ?></strong> <?= t('hours') ?></li>
+ <li><?= t('Spent:') ?> <strong><?= $this->e($task['time_spent']) ?></strong> <?= t('hours') ?></li>
+ <li><?= t('Remaining:') ?> <strong><?= $this->e($task['time_estimated'] - $task['time_spent']) ?></strong> <?= t('hours') ?></li>
+</ul>
+
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/task/transitions.php b/app/Template/task/transitions.php
new file mode 100644
index 00000000..6455fd66
--- /dev/null
+++ b/app/Template/task/transitions.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <h2><?= t('Transitions') ?></h2>
+</div>
+
+<?php if (empty($transitions)): ?>
+ <p class="alert"><?= t('There is nothing to show.') ?></p>
+<?php else: ?>
+ <table class="table-stripped">
+ <tr>
+ <th><?= t('Date') ?></th>
+ <th><?= t('Source column') ?></th>
+ <th><?= t('Destination column') ?></th>
+ <th><?= t('Executer') ?></th>
+ <th><?= t('Time spent in the column') ?></th>
+ </tr>
+ <?php foreach ($transitions as $transition): ?>
+ <tr>
+ <td><?= dt('%B %e, %Y at %k:%M %p', $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>
+ <td><?= n(round($transition['time_spent'] / 3600, 2)).' '.t('hours') ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/tasklink/create.php b/app/Template/tasklink/create.php
new file mode 100644
index 00000000..749f2968
--- /dev/null
+++ b/app/Template/tasklink/create.php
@@ -0,0 +1,37 @@
+<div class="page-header">
+ <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">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
+ <?= $this->form->hidden('opposite_task_id', $values) ?>
+
+ <?= $this->form->label(t('Label'), 'link_id') ?>
+ <?= $this->form->select('link_id', $labels, $values, $errors) ?>
+
+ <?= $this->form->label(t('Task'), 'title') ?>
+ <?= $this->form->text(
+ 'title',
+ $values,
+ $errors,
+ array(
+ 'required',
+ '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'])).'"',
+ ),
+ 'task-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 ?>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/tasklink/edit.php b/app/Template/tasklink/edit.php
new file mode 100644
index 00000000..73b43277
--- /dev/null
+++ b/app/Template/tasklink/edit.php
@@ -0,0 +1,34 @@
+<div class="page-header">
+ <h2><?= t('Edit link') ?></h2>
+</div>
+
+<form action="<?= $this->url->href('tasklink', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'link_id' => $task_link['id'])) ?>" method="post" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('task_id', $values) ?>
+ <?= $this->form->hidden('opposite_task_id', $values) ?>
+
+ <?= $this->form->label(t('Label'), 'link_id') ?>
+ <?= $this->form->select('link_id', $labels, $values, $errors) ?>
+
+ <?= $this->form->label(t('Task'), 'title') ?>
+ <?= $this->form->text(
+ 'title',
+ $values,
+ $errors,
+ array(
+ 'required',
+ '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'])).'"',
+ ),
+ 'task-autocomplete') ?>
+
+ <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/tasklink/remove.php b/app/Template/tasklink/remove.php
new file mode 100644
index 00000000..262fb488
--- /dev/null
+++ b/app/Template/tasklink/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 with task #%d?', $link['opposite_task_id']) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'tasklink', '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'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/tasklink/show.php b/app/Template/tasklink/show.php
new file mode 100644
index 00000000..7125b11d
--- /dev/null
+++ b/app/Template/tasklink/show.php
@@ -0,0 +1,104 @@
+<?php if (! empty($links)): ?>
+<div class="page-header">
+ <h2><?= t('Links') ?></h2>
+</div>
+<table id="links">
+ <tr>
+ <th class="column-20"><?= t('Label') ?></th>
+ <th class="column-30"><?= t('Task') ?></th>
+ <th><?= t('Column') ?></th>
+ <th><?= t('Assignee') ?></th>
+ <?php if (! isset($not_editable)): ?>
+ <th><?= t('Action') ?></th>
+ <?php endif ?>
+ </tr>
+ <?php foreach ($links as $label => $grouped_links): ?>
+ <?php $hide_td = false ?>
+ <?php foreach ($grouped_links as $link): ?>
+ <tr>
+ <?php if (! $hide_td): ?>
+ <td rowspan="<?= count($grouped_links) ?>"><?= t('This task') ?> <strong><?= t($label) ?></strong></td>
+ <?php $hide_td = true ?>
+ <?php endif ?>
+
+ <td>
+ <?php if (! isset($not_editable)): ?>
+ <?= $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 else: ?>
+ <?= $this->url->link(
+ $this->e('#'.$link['task_id'].' '.$link['title']),
+ 'task',
+ 'readonly',
+ array('task_id' => $link['task_id'], 'token' => $project['token']),
+ false,
+ $link['is_active'] ? '' : 'task-link-closed'
+ ) ?>
+ <?php endif ?>
+
+ <br/>
+
+ <?php if (! empty($link['task_time_spent'])): ?>
+ <strong><?= $this->e($link['task_time_spent']).'h' ?></strong> <?= t('spent') ?>
+ <?php endif ?>
+
+ <?php if (! empty($link['task_time_estimated'])): ?>
+ <strong><?= $this->e($link['task_time_estimated']).'h' ?></strong> <?= t('estimated') ?>
+ <?php endif ?>
+ </td>
+ <td><?= $this->e($link['column_title']) ?></td>
+ <td>
+ <?php if (! empty($link['task_assignee_username'])): ?>
+ <?php if (! isset($not_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)): ?>
+ <td>
+ <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>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ <?php endforeach ?>
+</table>
+
+<?php if (! isset($not_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() ?>
+ <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?>
+ <?= $this->form->hidden('opposite_task_id', array()) ?>
+
+ <?= $this->form->select('link_id', $link_label_list, array(), array()) ?>
+
+ <?= $this->form->text(
+ 'title',
+ array(),
+ array(),
+ array(
+ 'required',
+ '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'])).'"',
+ ),
+ 'task-autocomplete') ?>
+
+ <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
+ </form>
+<?php endif ?>
+
+<?php endif ?>
diff --git a/app/Template/timetable/index.php b/app/Template/timetable/index.php
new file mode 100644
index 00000000..7a63a2ec
--- /dev/null
+++ b/app/Template/timetable/index.php
@@ -0,0 +1,44 @@
+<div class="page-header">
+ <h2><?= t('Timetable') ?></h2>
+ <ul>
+ <li><?= $this->url->link(t('Day timetable'), 'timetableday', 'index', array('user_id' => $user['id'])) ?></li>
+ <li><?= $this->url->link(t('Week timetable'), 'timetableweek', 'index', array('user_id' => $user['id'])) ?></li>
+ <li><?= $this->url->link(t('Time off timetable'), 'timetableoff', 'index', array('user_id' => $user['id'])) ?></li>
+ <li><?= $this->url->link(t('Overtime timetable'), 'timetableextra', 'index', array('user_id' => $user['id'])) ?></li>
+ </ul>
+</div>
+
+<form method="get" action="?" autocomplete="off" class="form-inline">
+
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $this->form->hidden('user_id', $values) ?>
+
+ <?= $this->form->label(t('From'), 'from') ?>
+ <?= $this->form->text('from', $values, array(), array(), 'form-date') ?>
+
+ <?= $this->form->label(t('To'), 'to') ?>
+ <?= $this->form->text('to', $values, array(), array(), 'form-date') ?>
+
+ <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
+</form>
+
+<?php if (! empty($timetable)): ?>
+<hr/>
+<h3><?= t('Work timetable') ?></h3>
+<table class="table-fixed table-stripped">
+ <tr>
+ <th><?= t('Day') ?></th>
+ <th><?= t('Start') ?></th>
+ <th><?= t('End') ?></th>
+ </tr>
+ <?php foreach ($timetable as $slot): ?>
+ <tr>
+ <td><?= dt('%B %e, %Y', $slot[0]->getTimestamp()) ?></td>
+ <td><?= dt('%k:%M %p', $slot[0]->getTimestamp()) ?></td>
+ <td><?= dt('%k:%M %p', $slot[1]->getTimestamp()) ?></td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/timetable_day/index.php b/app/Template/timetable_day/index.php
new file mode 100644
index 00000000..d2877816
--- /dev/null
+++ b/app/Template/timetable_day/index.php
@@ -0,0 +1,45 @@
+<div class="page-header">
+ <h2><?= t('Day timetable') ?></h2>
+</div>
+
+<?php if (! empty($timetable)): ?>
+
+<table class="table-fixed table-stripped">
+ <tr>
+ <th><?= t('Start time') ?></th>
+ <th><?= t('End time') ?></th>
+ <th><?= t('Action') ?></th>
+ </tr>
+ <?php foreach ($timetable as $slot): ?>
+ <tr>
+ <td><?= $slot['start'] ?></td>
+ <td><?= $slot['end'] ?></td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'timetableday', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<h3><?= t('Add new time slot') ?></h3>
+<?php endif ?>
+
+<form method="post" action="<?= $this->url->href('timetableday', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->hidden('user_id', $values) ?>
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Start time'), 'start') ?>
+ <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
+
+ <?= $this->form->label(t('End time'), 'end') ?>
+ <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<p class="alert alert-info">
+ <?= t('This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.') ?>
+</p> \ No newline at end of file
diff --git a/app/Template/timetable_day/remove.php b/app/Template/timetable_day/remove.php
new file mode 100644
index 00000000..1b33b266
--- /dev/null
+++ b/app/Template/timetable_day/remove.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Remove time slot') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'timetableday', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'timetableday', 'index', array('user_id' => $user['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/timetable_extra/index.php b/app/Template/timetable_extra/index.php
new file mode 100644
index 00000000..d3224ae6
--- /dev/null
+++ b/app/Template/timetable_extra/index.php
@@ -0,0 +1,56 @@
+<div class="page-header">
+ <h2><?= t('Overtime timetable') ?></h2>
+</div>
+
+<?php if (! $paginator->isEmpty()): ?>
+
+<table class="table-fixed table-stripped">
+ <tr>
+ <th><?= $paginator->order(t('Day'), 'Day') ?></th>
+ <th><?= $paginator->order(t('All day'), 'all_day') ?></th>
+ <th><?= $paginator->order(t('Start time'), 'start') ?></th>
+ <th><?= $paginator->order(t('End time'), 'end') ?></th>
+ <th class="column-40"><?= t('Comment') ?></th>
+ <th><?= t('Action') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $slot): ?>
+ <tr>
+ <td><?= $slot['date'] ?></td>
+ <td><?= $slot['all_day'] == 1 ? t('Yes') : t('No') ?></td>
+ <td><?= $slot['start'] ?></td>
+ <td><?= $slot['end'] ?></td>
+ <td><?= $this->e($slot['comment']) ?></td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'timetableextra', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<?= $paginator ?>
+
+<?php endif ?>
+
+<form method="post" action="<?= $this->url->href('timetableextra', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->hidden('user_id', $values) ?>
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Day'), 'date') ?>
+ <?= $this->form->text('date', $values, $errors, array('required'), 'form-date') ?>
+
+ <?= $this->form->checkbox('all_day', t('All day'), 1) ?>
+
+ <?= $this->form->label(t('Start time'), 'start') ?>
+ <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
+
+ <?= $this->form->label(t('End time'), 'end') ?>
+ <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
+
+ <?= $this->form->label(t('Comment'), 'comment') ?>
+ <?= $this->form->text('comment', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/timetable_extra/remove.php b/app/Template/timetable_extra/remove.php
new file mode 100644
index 00000000..fc907438
--- /dev/null
+++ b/app/Template/timetable_extra/remove.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Remove time slot') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'timetableextra', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'timetableextra', 'index', array('user_id' => $user['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/timetable_off/index.php b/app/Template/timetable_off/index.php
new file mode 100644
index 00000000..75e02dbd
--- /dev/null
+++ b/app/Template/timetable_off/index.php
@@ -0,0 +1,56 @@
+<div class="page-header">
+ <h2><?= t('Time off timetable') ?></h2>
+</div>
+
+<?php if (! $paginator->isEmpty()): ?>
+
+<table class="table-fixed table-stripped">
+ <tr>
+ <th><?= $paginator->order(t('Day'), 'Day') ?></th>
+ <th><?= $paginator->order(t('All day'), 'all_day') ?></th>
+ <th><?= $paginator->order(t('Start time'), 'start') ?></th>
+ <th><?= $paginator->order(t('End time'), 'end') ?></th>
+ <th class="column-40"><?= t('Comment') ?></th>
+ <th><?= t('Action') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $slot): ?>
+ <tr>
+ <td><?= $slot['date'] ?></td>
+ <td><?= $slot['all_day'] == 1 ? t('Yes') : t('No') ?></td>
+ <td><?= $slot['start'] ?></td>
+ <td><?= $slot['end'] ?></td>
+ <td><?= $this->e($slot['comment']) ?></td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'timetableoff', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<?= $paginator ?>
+
+<?php endif ?>
+
+<form method="post" action="<?= $this->url->href('timetableoff', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->hidden('user_id', $values) ?>
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Day'), 'date') ?>
+ <?= $this->form->text('date', $values, $errors, array('required'), 'form-date') ?>
+
+ <?= $this->form->checkbox('all_day', t('All day'), 1) ?>
+
+ <?= $this->form->label(t('Start time'), 'start') ?>
+ <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
+
+ <?= $this->form->label(t('End time'), 'end') ?>
+ <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
+
+ <?= $this->form->label(t('Comment'), 'comment') ?>
+ <?= $this->form->text('comment', $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/timetable_off/remove.php b/app/Template/timetable_off/remove.php
new file mode 100644
index 00000000..621e191c
--- /dev/null
+++ b/app/Template/timetable_off/remove.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Remove time slot') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'timetableoff', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'timetableoff', 'index', array('user_id' => $user['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/timetable_week/index.php b/app/Template/timetable_week/index.php
new file mode 100644
index 00000000..552e9302
--- /dev/null
+++ b/app/Template/timetable_week/index.php
@@ -0,0 +1,46 @@
+<div class="page-header">
+ <h2><?= t('Week timetable') ?></h2>
+</div>
+
+<?php if (! empty($timetable)): ?>
+
+<table class="table-fixed table-stripped">
+ <tr>
+ <th><?= t('Day') ?></th>
+ <th><?= t('Start time') ?></th>
+ <th><?= t('End time') ?></th>
+ <th><?= t('Action') ?></th>
+ </tr>
+ <?php foreach ($timetable as $slot): ?>
+ <tr>
+ <td><?= $this->datetime->getWeekDay($slot['day']) ?></td>
+ <td><?= $slot['start'] ?></td>
+ <td><?= $slot['end'] ?></td>
+ <td>
+ <?= $this->url->link(t('Remove'), 'timetableweek', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<h3><?= t('Add new time slot') ?></h3>
+<?php endif ?>
+
+<form method="post" action="<?= $this->url->href('timetableweek', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->hidden('user_id', $values) ?>
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Day'), 'day') ?>
+ <?= $this->form->select('day', $this->datetime->getWeekDays(), $values, $errors) ?>
+
+ <?= $this->form->label(t('Start time'), 'start') ?>
+ <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?>
+
+ <?= $this->form->label(t('End time'), 'end') ?>
+ <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/timetable_week/remove.php b/app/Template/timetable_week/remove.php
new file mode 100644
index 00000000..f5a10199
--- /dev/null
+++ b/app/Template/timetable_week/remove.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Remove time slot') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'timetableweek', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'timetableweek', 'index', array('user_id' => $user['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/twofactor/check.php b/app/Template/twofactor/check.php
new file mode 100644
index 00000000..68a58a6c
--- /dev/null
+++ b/app/Template/twofactor/check.php
@@ -0,0 +1,10 @@
+<form method="post" action="<?= $this->url->href('twofactor', 'check', array('user_id' => $this->user->getId())) ?>" 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/twofactor/disable.php b/app/Template/twofactor/disable.php
new file mode 100644
index 00000000..36be4ef9
--- /dev/null
+++ b/app/Template/twofactor/disable.php
@@ -0,0 +1,14 @@
+<div class="page-header">
+ <h2><?= t('Disable two factor authentication') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to disable the two factor authentication for this user: "%s"?', $user['name'] ?: $user['username']) ?>
+ </p>
+
+ <div class="form-actions">
+ <?= $this->url->link(t('Yes'), 'twofactor', 'disable', array('user_id' => $user['id'], 'disable' => 'yes'), true, 'btn btn-red') ?>
+ <?= t('or') ?> <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/twofactor/index.php b/app/Template/twofactor/index.php
new file mode 100644
index 00000000..36b92653
--- /dev/null
+++ b/app/Template/twofactor/index.php
@@ -0,0 +1,37 @@
+<div class="page-header">
+ <h2><?= t('Two factor authentication') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->url->href('twofactor', 'save', 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') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+<?php endif ?>
diff --git a/app/Template/user/calendar.php b/app/Template/user/calendar.php
new file mode 100644
index 00000000..7ec12496
--- /dev/null
+++ b/app/Template/user/calendar.php
@@ -0,0 +1,6 @@
+<div id="user-calendar"
+ data-check-url="<?= $this->url->href('calendar', 'user') ?>"
+ data-user-id="<?= $user['id'] ?>"
+ data-save-url="<?= $this->url->href('calendar', 'save') ?>"
+>
+</div> \ No newline at end of file
diff --git a/app/Template/user/edit.php b/app/Template/user/edit.php
new file mode 100644
index 00000000..e29dcfca
--- /dev/null
+++ b/app/Template/user/edit.php
@@ -0,0 +1,42 @@
+<div class="page-header">
+ <h2><?= t('Edit user') ?></h2>
+</div>
+<form method="post" action="<?= $this->url->href('user', 'edit', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->hidden('is_ldap_user', $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->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Email'), 'email') ?>
+ <?= $this->form->email('email', $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Default project'), 'default_project_id') ?>
+ <?= $this->form->select('default_project_id', $projects, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Timezone'), 'timezone') ?>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Language'), 'language') ?>
+ <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+
+ <div class="alert alert-error">
+ <?= $this->form->checkbox('disable_login_form', t('Disable login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?><br/>
+
+ <?php if ($this->user->isAdmin()): ?>
+ <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1) ?><br/>
+ <?php endif ?>
+ </div>
+
+ <div class="form-actions">
+ <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>
+</form> \ No newline at end of file
diff --git a/app/Templates/user_external.php b/app/Template/user/external.php
index 676b2c73..df85ace7 100644
--- a/app/Templates/user_external.php
+++ b/app/Template/user/external.php
@@ -6,11 +6,11 @@
<h3><i class="fa fa-google"></i> <?= t('Google Account') ?></h3>
<p class="listing">
- <?php if (Helper\is_current_user($user['id'])): ?>
+ <?php if ($this->user->isCurrentUser($user['id'])): ?>
<?php if (empty($user['google_id'])): ?>
- <a href="?controller=user&amp;action=google<?= Helper\param_csrf() ?>"><?= t('Link my Google Account') ?></a>
+ <?= $this->url->link(t('Link my Google Account'), 'user', 'google', array(), true) ?>
<?php else: ?>
- <a href="?controller=user&amp;action=unlinkGoogle<?= Helper\param_csrf() ?>"><?= t('Unlink my Google Account') ?></a>
+ <?= $this->url->link(t('Unlink my Google Account'), 'user', 'unlinkGoogle', array(), true) ?>
<?php endif ?>
<?php else: ?>
<?= empty($user['google_id']) ? t('No account linked.') : t('Account linked.') ?>
@@ -22,11 +22,11 @@
<h3><i class="fa fa-github"></i> <?= t('Github Account') ?></h3>
<p class="listing">
- <?php if (Helper\is_current_user($user['id'])): ?>
+ <?php if ($this->user->isCurrentUser($user['id'])): ?>
<?php if (empty($user['github_id'])): ?>
- <a href="?controller=user&amp;action=gitHub<?= Helper\param_csrf() ?>"><?= t('Link my GitHub Account') ?></a>
+ <?= $this->url->link(t('Link my GitHub Account'), 'user', 'github', array(), true) ?>
<?php else: ?>
- <a href="?controller=user&amp;action=unlinkGitHub<?= Helper\param_csrf() ?>"><?= t('Unlink my GitHub Account') ?></a>
+ <?= $this->url->link(t('Unlink my GitHub Account'), 'user', 'unlinkGitHub', array(), true) ?>
<?php endif ?>
<?php else: ?>
<?= empty($user['github_id']) ? t('No account linked.') : t('Account linked.') ?>
diff --git a/app/Template/user/index.php b/app/Template/user/index.php
new file mode 100644
index 00000000..6b4396b2
--- /dev/null
+++ b/app/Template/user/index.php
@@ -0,0 +1,76 @@
+<section id="main">
+ <div class="page-header">
+ <?php if ($this->user->isAdmin()): ?>
+ <ul>
+ <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New user'), 'user', 'create') ?></li>
+ </ul>
+ <?php endif ?>
+ </div>
+ <section>
+ <?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('No user') ?></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><?= $paginator->order(t('Administrator'), 'is_admin') ?></th>
+ <th><?= $paginator->order(t('Two factor authentication'), 'twofactor_activated') ?></th>
+ <th><?= $paginator->order(t('Default project'), 'default_project_id') ?></th>
+ <th><?= $paginator->order(t('Notifications'), 'notifications_enabled') ?></th>
+ <th><?= t('External accounts') ?></th>
+ <th><?= $paginator->order(t('Account type'), 'is_ldap_user') ?></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>
+ <?= $user['is_admin'] ? t('Yes') : t('No') ?>
+ </td>
+ <td>
+ <?= $user['twofactor_activated'] ? t('Yes') : t('No') ?>
+ </td>
+ <td>
+ <?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? $this->e($projects[$user['default_project_id']]) : t('None'); ?>
+ </td>
+ <td>
+ <?php if ($user['notifications_enabled'] == 1): ?>
+ <?= t('Enabled') ?>
+ <?php else: ?>
+ <?= t('Disabled') ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <ul class="no-bullet">
+ <?php if ($user['google_id']): ?>
+ <li><i class="fa fa-google fa-fw"></i><?= t('Google account linked') ?></li>
+ <?php endif ?>
+ <?php if ($user['github_id']): ?>
+ <li><i class="fa fa-github fa-fw"></i><?= t('Github account linked') ?></li>
+ <?php endif ?>
+ </ul>
+ </td>
+ <td>
+ <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+ <?php endif ?>
+ </section>
+</section>
diff --git a/app/Templates/user_last.php b/app/Template/user/last.php
index 0b55b0d5..ab25f79b 100644
--- a/app/Templates/user_last.php
+++ b/app/Template/user/last.php
@@ -15,9 +15,9 @@
<?php foreach($last_logins as $login): ?>
<tr>
<td><?= dt('%B %e, %Y at %k:%M %p', $login['date_creation']) ?></td>
- <td><?= Helper\escape($login['auth_type']) ?></td>
- <td><?= Helper\escape($login['ip']) ?></td>
- <td><?= Helper\escape(Helper\summary($login['user_agent'])) ?></td>
+ <td><?= $this->e($login['auth_type']) ?></td>
+ <td><?= $this->e($login['ip']) ?></td>
+ <td><?= $this->e($this->text->truncate($login['user_agent'])) ?></td>
</tr>
<?php endforeach ?>
</table>
diff --git a/app/Template/user/layout.php b/app/Template/user/layout.php
new file mode 100644
index 00000000..e60ab77d
--- /dev/null
+++ b/app/Template/user/layout.php
@@ -0,0 +1,18 @@
+<section id="main">
+ <div class="page-header">
+ <?php if ($this->user->isAdmin()): ?>
+ <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 user'), 'user', 'create') ?></li>
+ </ul>
+ <?php endif ?>
+ </div>
+ <section class="sidebar-container" id="user-section">
+
+ <?= $this->render('user/sidebar', array('user' => $user)) ?>
+
+ <div class="sidebar-content">
+ <?= $user_content_for_layout ?>
+ </div>
+ </section>
+</section> \ No newline at end of file
diff --git a/app/Template/user/new.php b/app/Template/user/new.php
new file mode 100644
index 00000000..ba7a3881
--- /dev/null
+++ b/app/Template/user/new.php
@@ -0,0 +1,45 @@
+<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>
+ </ul>
+ </div>
+ <section>
+ <form method="post" action="<?= $this->url->href('user', 'save') ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Username'), 'username') ?>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?><br/>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Email'), 'email') ?>
+ <?= $this->form->email('email', $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Password'), 'password') ?>
+ <?= $this->form->password('password', $values, $errors, array('required')) ?><br/>
+
+ <?= $this->form->label(t('Confirmation'), 'confirmation') ?>
+ <?= $this->form->password('confirmation', $values, $errors, array('required')) ?><br/>
+
+ <?= $this->form->label(t('Default project'), 'default_project_id') ?>
+ <?= $this->form->select('default_project_id', $projects, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Timezone'), 'timezone') ?>
+ <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/>
+
+ <?= $this->form->label(t('Language'), 'language') ?>
+ <?= $this->form->select('language', $languages, $values, $errors) ?><br/>
+
+ <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'user', 'index') ?>
+ </div>
+ </form>
+ </section>
+</section> \ No newline at end of file
diff --git a/app/Template/user/notifications.php b/app/Template/user/notifications.php
new file mode 100644
index 00000000..a425705d
--- /dev/null
+++ b/app/Template/user/notifications.php
@@ -0,0 +1,38 @@
+<div class="page-header">
+ <h2><?= t('Email notifications') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->url->href('user', 'notifications', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+ <?= $this->form->checkbox('notifications_enabled', t('Enable email notifications'), '1', $notifications['notifications_enabled'] == 1) ?><br>
+
+ <hr>
+
+ <?= t('I want to receive notifications for:') ?>
+
+ <?= $this->form->radios('notifications_filter', array(
+ \Model\Notification::FILTER_NONE => t('All tasks'),
+ \Model\Notification::FILTER_ASSIGNEE => t('Only for tasks assigned to me'),
+ \Model\Notification::FILTER_CREATOR => t('Only for tasks created by me'),
+ \Model\Notification::FILTER_BOTH => t('Only for tasks created by me and assigned to me'),
+ ), $notifications) ?><br>
+
+ <hr>
+
+ <?php if (! empty($projects)): ?>
+ <p><?= t('I want to receive notifications only for those projects:') ?><br/><br/></p>
+
+ <div class="form-checkbox-group">
+ <?php foreach ($projects as $project_id => $project_name): ?>
+ <?= $this->form->checkbox('projects['.$project_id.']', $project_name, '1', isset($notifications['project_'.$project_id])) ?><br>
+ <?php endforeach ?>
+ </div>
+ <?php endif ?>
+
+ <div class="form-actions">
+ <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>
+</form> \ No newline at end of file
diff --git a/app/Template/user/password.php b/app/Template/user/password.php
new file mode 100644
index 00000000..3ef28d33
--- /dev/null
+++ b/app/Template/user/password.php
@@ -0,0 +1,26 @@
+<div class="page-header">
+ <h2><?= t('Password modification') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->url->href('user', 'password', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->form->hidden('id', $values) ?>
+ <?= $this->form->csrf() ?>
+
+ <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/>
+ </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->label(t('Confirmation'), 'confirmation') ?>
+ <?= $this->form->password('confirmation', $values, $errors) ?><br/>
+
+ <div class="form-actions">
+ <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>
+</form>
diff --git a/app/Template/user/remove.php b/app/Template/user/remove.php
new file mode 100644
index 00000000..810a3a3f
--- /dev/null
+++ b/app/Template/user/remove.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Remove user') ?></h2>
+</div>
+
+<div class="confirm">
+ <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') ?>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Templates/user_sessions.php b/app/Template/user/sessions.php
index b647d726..a7504a7a 100644
--- a/app/Templates/user_sessions.php
+++ b/app/Template/user/sessions.php
@@ -17,9 +17,9 @@
<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><?= Helper\escape($session['ip']) ?></td>
- <td><?= Helper\escape(Helper\summary($session['user_agent'])) ?></td>
- <td><a href="?controller=user&amp;action=removeSession&amp;user_id=<?= $user['id'] ?>&amp;id=<?= $session['id'].Helper\param_csrf() ?>"><?= t('Remove') ?></a></td>
+ <td><?= $this->e($session['ip']) ?></td>
+ <td><?= $this->e($this->text->truncate($session['user_agent'])) ?></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/share.php b/app/Template/user/share.php
new file mode 100644
index 00000000..8f333a6b
--- /dev/null
+++ b/app/Template/user/share.php
@@ -0,0 +1,17 @@
+<div class="page-header">
+ <h2><?= t('Public access') ?></h2>
+</div>
+
+<?php if (! empty($user['token'])): ?>
+
+ <div class="listing">
+ <ul class="no-bullet">
+ <li><strong><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li>
+ </ul>
+ </div>
+
+ <?= $this->url->link(t('Disable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'disable'), true, 'btn btn-red') ?>
+
+<?php else: ?>
+ <?= $this->url->link(t('Enable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?>
+<?php endif ?>
diff --git a/app/Template/user/show.php b/app/Template/user/show.php
new file mode 100644
index 00000000..5442e2e7
--- /dev/null
+++ b/app/Template/user/show.php
@@ -0,0 +1,39 @@
+<div class="page-header">
+ <h2><?= t('Summary') ?></h2>
+</div>
+<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>
+
+<div class="page-header">
+ <h2><?= t('Security') ?></h2>
+</div>
+<ul class="listing">
+ <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : t('Regular user') ?></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>
+
+<div class="page-header">
+ <h2><?= t('Preferences') ?></h2>
+</div>
+<ul class="listing">
+ <li><?= t('Default project:') ?> <strong><?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? $this->e($projects[$user['default_project_id']]) : t('None') ?></strong></li>
+ <li><?= t('Timezone:') ?> <strong><?= $this->text->in($user['timezone'], $timezones) ?></strong></li>
+ <li><?= t('Language:') ?> <strong><?= $this->text->in($user['language'], $languages) ?></strong></li>
+ <li><?= t('Notifications:') ?> <strong><?= $user['notifications_enabled'] == 1 ? t('Enabled') : t('Disabled') ?></strong></li>
+</ul>
+
+<?php if (! empty($user['token'])): ?>
+ <div class="page-header">
+ <h2><?= t('Public access') ?></h2>
+ </div>
+
+ <div class="listing">
+ <ul class="no-bullet">
+ <li><strong><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li>
+ </ul>
+ </div>
+<?php endif ?>
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
new file mode 100644
index 00000000..2c8e909a
--- /dev/null
+++ b/app/Template/user/sidebar.php
@@ -0,0 +1,77 @@
+<div class="sidebar">
+ <h2><?= t('Information') ?></h2>
+ <ul>
+ <li>
+ <?= $this->url->link(t('Summary'), 'user', 'show', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php if ($this->user->isAdmin()): ?>
+ <li>
+ <?= $this->url->link(t('User dashboard'), 'app', 'dashboard', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('User calendar'), 'user', 'calendar', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+ <?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?>
+ <li>
+ <?= $this->url->link(t('Time tracking'), 'user', 'timesheet', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Last logins'), 'user', 'last', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+ </ul>
+
+ <h2><?= t('Actions') ?></h2>
+ <ul>
+ <?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?>
+ <li>
+ <?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
+ </li>
+
+ <?php if ($user['is_ldap_user'] == 0): ?>
+ <li>
+ <?= $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->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->url->link(t('Two factor authentication'), 'twofactor', 'disable', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+
+ <li>
+ <?= $this->url->link(t('Public access'), 'user', 'share', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Email notifications'), 'user', 'notifications', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('External accounts'), 'user', 'external', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+
+ <?php if ($this->user->isAdmin()): ?>
+ <li>
+ <?= $this->url->link(t('Hourly rates'), 'hourlyrate', 'index', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->url->link(t('Manage timetable'), 'timetable', 'index', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+
+ <?php if ($this->user->isAdmin() && ! $this->user->isCurrentUser($user['id'])): ?>
+ <li>
+ <?= $this->url->link(t('Remove'), 'user', 'remove', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/user/timesheet.php b/app/Template/user/timesheet.php
new file mode 100644
index 00000000..5c0d3af8
--- /dev/null
+++ b/app/Template/user/timesheet.php
@@ -0,0 +1,29 @@
+<div class="page-header">
+ <h2><?= t('Time Tracking') ?></h2>
+</div>
+
+<h3><?= t('Subtask timesheet') ?></h3>
+<?php if ($subtask_paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is nothing to show.') ?></p>
+<?php else: ?>
+ <table class="table-fixed">
+ <tr>
+ <th class="column-25"><?= $subtask_paginator->order(t('Task'), 'task_title') ?></th>
+ <th class="column-25"><?= $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>
+ </tr>
+ <?php foreach ($subtask_paginator->getCollection() as $record): ?>
+ <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><?= n($record['time_spent']).' '.t('hours') ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $subtask_paginator ?>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Templates/action_event.php b/app/Templates/action_event.php
deleted file mode 100644
index eee41780..00000000
--- a/app/Templates/action_event.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<div class="page-header">
- <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
-</div>
-
-<h3><?= t('Choose an event') ?></h3>
-<form method="post" action="?controller=action&amp;action=params&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('project_id', $values) ?>
- <?= Helper\form_hidden('action_name', $values) ?>
-
- <?= Helper\form_label(t('Event'), 'event_name') ?>
- <?= Helper\form_select('event_name', $events, $values) ?><br/>
-
- <div class="form-help">
- <?= t('When the selected event occurs execute the corresponding action.') ?>
- </div>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/action_index.php b/app/Templates/action_index.php
deleted file mode 100644
index 30874591..00000000
--- a/app/Templates/action_index.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<div class="page-header">
- <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
-</div>
-
-<?php if (! empty($actions)): ?>
-
-<h3><?= t('Defined actions') ?></h3>
-<table>
- <tr>
- <th><?= t('Event name') ?></th>
- <th><?= t('Action name') ?></th>
- <th><?= t('Action parameters') ?></th>
- <th><?= t('Action') ?></th>
- </tr>
-
- <?php foreach ($actions as $action): ?>
- <tr>
- <td><?= Helper\in_list($action['event_name'], $available_events) ?></td>
- <td><?= Helper\in_list($action['action_name'], $available_actions) ?></td>
- <td>
- <ul>
- <?php foreach ($action['params'] as $param): ?>
- <li>
- <?= Helper\in_list($param['name'], $available_params) ?> =
- <strong>
- <?php if (Helper\contains($param['name'], 'column_id')): ?>
- <?= Helper\in_list($param['value'], $columns_list) ?>
- <?php elseif (Helper\contains($param['name'], 'user_id')): ?>
- <?= Helper\in_list($param['value'], $users_list) ?>
- <?php elseif (Helper\contains($param['name'], 'project_id')): ?>
- <?= Helper\in_list($param['value'], $projects_list) ?>
- <?php elseif (Helper\contains($param['name'], 'color_id')): ?>
- <?= Helper\in_list($param['value'], $colors_list) ?>
- <?php elseif (Helper\contains($param['name'], 'category_id')): ?>
- <?= Helper\in_list($param['value'], $categories_list) ?>
- <?php elseif (Helper\contains($param['name'], 'label')): ?>
- <?= Helper\escape($param['value']) ?>
- <?php endif ?>
- </strong>
- </li>
- <?php endforeach ?>
- </ul>
- </td>
- <td>
- <a href="?controller=action&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;action_id=<?= $action['id'] ?>"><?= t('Remove') ?></a>
- </td>
- </tr>
- <?php endforeach ?>
-
-</table>
-
-<?php endif ?>
-
-<h3><?= t('Add an action') ?></h3>
-<form method="post" action="?controller=action&amp;action=event&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('project_id', $values) ?>
-
- <?= Helper\form_label(t('Action'), 'action_name') ?>
- <?= Helper\form_select('action_name', $available_actions, $values) ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/action_params.php b/app/Templates/action_params.php
deleted file mode 100644
index f647149b..00000000
--- a/app/Templates/action_params.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<div class="page-header">
- <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2>
-</div>
-
-<h3><?= t('Define action parameters') ?></h3>
-<form method="post" action="?controller=action&amp;action=create&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('project_id', $values) ?>
- <?= Helper\form_hidden('event_name', $values) ?>
- <?= Helper\form_hidden('action_name', $values) ?>
-
- <?php foreach ($action_params as $param_name => $param_desc): ?>
-
- <?php if (Helper\contains($param_name, 'column_id')): ?>
- <?= Helper\form_label($param_desc, $param_name) ?>
- <?= Helper\form_select('params['.$param_name.']', $columns_list, $values) ?><br/>
- <?php elseif (Helper\contains($param_name, 'user_id')): ?>
- <?= Helper\form_label($param_desc, $param_name) ?>
- <?= Helper\form_select('params['.$param_name.']', $users_list, $values) ?><br/>
- <?php elseif (Helper\contains($param_name, 'project_id')): ?>
- <?= Helper\form_label($param_desc, $param_name) ?>
- <?= Helper\form_select('params['.$param_name.']', $projects_list, $values) ?><br/>
- <?php elseif (Helper\contains($param_name, 'color_id')): ?>
- <?= Helper\form_label($param_desc, $param_name) ?>
- <?= Helper\form_select('params['.$param_name.']', $colors_list, $values) ?><br/>
- <?php elseif (Helper\contains($param_name, 'category_id')): ?>
- <?= Helper\form_label($param_desc, $param_name) ?>
- <?= Helper\form_select('params['.$param_name.']', $categories_list, $values) ?><br/>
- <?php elseif (Helper\contains($param_name, 'label')): ?>
- <?= Helper\form_label($param_desc, $param_name) ?>
- <?= Helper\form_text('params['.$param_name.']', $values) ?>
- <?php endif ?>
-
- <?php endforeach ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <a href="?controller=action&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/action_remove.php b/app/Templates/action_remove.php
deleted file mode 100644
index 668067da..00000000
--- a/app/Templates/action_remove.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="page-header">
- <h2><?= t('Remove an automatic action') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info">
- <?= t('Do you really want to remove this action: "%s"?', Helper\in_list($action['event_name'], $available_events).'/'.Helper\in_list($action['action_name'], $available_actions)) ?>
- </p>
-
- <div class="form-actions">
- <?= Helper\a(t('Yes'), 'action', 'remove', array('project_id' => $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?>
- <?= t('or') ?> <?= Helper\a(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/app_index.php b/app/Templates/app_index.php
deleted file mode 100644
index 91eecce4..00000000
--- a/app/Templates/app_index.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('Dashboard') ?></h2>
- </div>
- <section id="dashboard">
- <div class="dashboard-left-column">
- <h2><?= t('My tasks') ?></h2>
- <?php if (empty($tasks)): ?>
- <p class="alert"><?= t('There is nothing assigned to you.') ?></p>
- <?php else: ?>
- <table>
- <tr>
- <th>&nbsp;</th>
- <th width="15%"><?= t('Project') ?></th>
- <th width="40%"><?= t('Title') ?></th>
- <th><?= t('Due date') ?></th>
- <th><?= t('Date created') ?></th>
- </tr>
- <?php foreach ($tasks as $task): ?>
- <tr>
- <td class="task-table task-<?= $task['color_id'] ?>">
- <?= Helper\a('#'.$task['id'], 'task', 'show', array('task_id' => $task['id'])) ?>
- </td>
- <td>
- <?= Helper\a(Helper\escape($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?>
- </td>
- <td>
- <?= Helper\a(Helper\escape($task['title']), 'task', 'show', array('task_id' => $task['id'])) ?>
- </td>
- <td>
- <?= dt('%B %e, %Y', $task['date_due']) ?>
- </td>
- <td>
- <?= dt('%B %e, %Y', $task['date_creation']) ?>
- </td>
- </tr>
- <?php endforeach ?>
- </table>
- <?php endif ?>
- </div>
- <div class="dashboard-right-column">
- <h2><?= t('Activity stream') ?></h2>
- <?= Helper\template('project_events', array('events' => $events)) ?>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/app_notfound.php b/app/Templates/app_notfound.php
deleted file mode 100644
index 734d16a4..00000000
--- a/app/Templates/app_notfound.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('Page not found') ?></h2>
- </div>
-
- <p class="alert alert-error">
- <?= t('Sorry, I didn\'t found this information in my database!') ?>
- </p>
-</section> \ No newline at end of file
diff --git a/app/Templates/board_assignee.php b/app/Templates/board_assignee.php
deleted file mode 100644
index 41ede32b..00000000
--- a/app/Templates/board_assignee.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<section id="main">
-
- <div class="page-header board">
- <h2><?= t('Project "%s"', $current_project_name) ?></h2>
- </div>
-
- <section>
- <h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3>
- <form method="post" action="?controller=board&amp;action=updateAssignee" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_hidden('project_id', $values) ?>
-
- <?= Helper\form_label(t('Assignee'), 'owner_id') ?>
- <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <a href="?controller=board&amp;action=show&amp;project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
- </div>
- </form>
- </section>
-
-</section> \ No newline at end of file
diff --git a/app/Templates/board_category.php b/app/Templates/board_category.php
deleted file mode 100644
index 36126a1d..00000000
--- a/app/Templates/board_category.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<section id="main">
-
- <div class="page-header board">
- <h2><?= t('Project "%s"', $current_project_name) ?></h2>
- </div>
-
- <section>
- <h3><?= t('Change category for the task "%s"', $values['title']) ?></h3>
- <form method="post" action="?controller=board&amp;action=updateCategory" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_hidden('project_id', $values) ?>
-
- <?= Helper\form_label(t('Category'), 'category_id') ?>
- <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <a href="?controller=board&amp;action=show&amp;project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
- </div>
- </form>
- </section>
-
-</section> \ No newline at end of file
diff --git a/app/Templates/board_edit.php b/app/Templates/board_edit.php
deleted file mode 100644
index cfaebc50..00000000
--- a/app/Templates/board_edit.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<div class="page-header">
- <h2><?= t('Edit the board for "%s"', $project['name']) ?></h2>
-</div>
-<section>
-
-<h3><?= t('Change columns') ?></h3>
-<form method="post" action="?controller=board&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?php $i = 0; ?>
- <table>
- <tr>
- <th><?= t('Position') ?></th>
- <th><?= t('Column title') ?></th>
- <th><?= t('Task limit') ?></th>
- <th><?= t('Actions') ?></th>
- </tr>
- <?php foreach ($columns as $column): ?>
- <tr>
- <td><?= Helper\form_label(t('Column %d', ++$i), 'title['.$column['id'].']', array('title="column_id='.$column['id'].'"')) ?></td>
- <td><?= Helper\form_text('title['.$column['id'].']', $values, $errors, array('required')) ?></td>
- <td><?= Helper\form_number('task_limit['.$column['id'].']', $values, $errors, array('placeholder="'.t('limit').'"')) ?></td>
- <td>
- <ul>
- <?php if ($column['position'] != 1): ?>
- <li>
- <?= Helper\a(t('Move Up'), 'board', 'moveColumn', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?>
- </li>
- <?php endif ?>
- <?php if ($column['position'] != count($columns)): ?>
- <li>
- <?= Helper\a(t('Move Down'), 'board', 'moveColumn', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?>
- </li>
- <?php endif ?>
- <li>
- <?= Helper\a(t('Remove'), 'board', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>
- </li>
- </ul>
- </td>
- </tr>
- <?php endforeach ?>
- </table>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/>
- </div>
-</form>
-
-<h3><?= t('Add a new column') ?></h3>
-<form method="post" action="?controller=board&amp;action=add&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('project_id', $values) ?>
- <?= Helper\form_label(t('Title'), 'title') ?>
- <?= Helper\form_text('title', $values, $errors, array('required')) ?>
-
- <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/Templates/board_index.php b/app/Templates/board_index.php
deleted file mode 100644
index bff7dcc9..00000000
--- a/app/Templates/board_index.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<section id="main">
-
- <div class="page-header board">
- <h2>
- <?= t('Project "%s"', $current_project_name) ?>
- </h2>
- </div>
-
- <div class="project-menu">
- <ul>
- <li>
- <span class="hide-tablet"><?= t('Filter by user') ?></span>
- <?= Helper\form_select('user_id', $users, $filters) ?>
- </li>
- <li>
- <span class="hide-tablet"><?= t('Filter by category') ?></span>
- <?= Helper\form_select('category_id', $categories, $filters) ?>
- </li>
- <li><a href="#" id="filter-due-date"><?= t('Filter by due date') ?></a></li>
- <li><a href="?controller=project&amp;action=search&amp;project_id=<?= $current_project_id ?>"><?= t('Search') ?></a></li>
- <li><a href="?controller=project&amp;action=tasks&amp;project_id=<?= $current_project_id ?>"><?= t('Completed tasks') ?></a></li>
- <li><a href="?controller=project&amp;action=activity&amp;project_id=<?= $current_project_id ?>"><?= t('Activity') ?></a></li>
- </ul>
- </div>
-
- <?php if (empty($board)): ?>
- <p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
- <?php else: ?>
- <?= Helper\template('board_show', array(
- 'current_project_id' => $current_project_id,
- 'board' => $board,
- 'categories' => $categories,
- 'board_private_refresh_interval' => $board_private_refresh_interval,
- 'board_highlight_period' => $board_highlight_period,
- )) ?>
- <?php endif ?>
-
-</section>
diff --git a/app/Templates/board_public.php b/app/Templates/board_public.php
deleted file mode 100644
index 85c90cfa..00000000
--- a/app/Templates/board_public.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<section id="main" class="public-board">
-
- <?php if (empty($columns)): ?>
- <p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
- <?php else: ?>
- <table id="board">
- <tr>
- <?php $column_with = round(100 / count($columns), 2); ?>
- <?php foreach ($columns as $column): ?>
- <th width="<?= $column_with ?>%">
- <?= Helper\escape($column['title']) ?>
- <?php if ($column['task_limit']): ?>
- <span title="<?= t('Task limit') ?>" class="task-limit">(<?= Helper\escape(count($column['tasks']).'/'.$column['task_limit']) ?>)</span>
- <?php endif ?>
- </th>
- <?php endforeach ?>
- </tr>
- <tr>
- <?php foreach ($columns as $column): ?>
- <td class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>">
- <?php foreach ($column['tasks'] as $task): ?>
- <div class="task-board task-<?= $task['color_id'] ?>">
-
- <?= Helper\template('board_task', array('task' => $task, 'categories' => $categories, 'not_editable' => true, 'project' => $project)) ?>
-
- </div>
- <?php endforeach ?>
- </td>
- <?php endforeach ?>
- </tr>
- </table>
- <?php endif ?>
-
-</section> \ No newline at end of file
diff --git a/app/Templates/board_show.php b/app/Templates/board_show.php
deleted file mode 100644
index e8c3c1ba..00000000
--- a/app/Templates/board_show.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<table id="board" data-project-id="<?= $current_project_id ?>" data-time="<?= time() ?>" data-check-interval="<?= $board_private_refresh_interval ?>" data-csrf-token=<?= \Core\Security::getCSRFToken() ?>>
-<tr>
- <?php $column_with = round(100 / count($board), 2); ?>
- <?php foreach ($board as $column): ?>
- <th width="<?= $column_with ?>%">
- <div class="board-add-icon">
- <a href="?controller=task&amp;action=create&amp;project_id=<?= $column['project_id'] ?>&amp;column_id=<?= $column['id'] ?>" title="<?= t('Add a new task') ?>">+</a>
- </div>
- <?= Helper\escape($column['title']) ?>
- <?php if ($column['task_limit']): ?>
- <span title="<?= t('Task limit') ?>" class="task-limit">
- (
- <span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span>
- /
- <?= Helper\escape($column['task_limit']) ?>
- )
- </span>
- <?php else: ?>
- <span title="<?= t('Task count') ?>" class="task-count">
- (<span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span>)
- </span>
- <?php endif ?>
- </th>
- <?php endforeach ?>
-</tr>
-<tr>
- <?php foreach ($board as $column): ?>
- <td
- id="column-<?= $column['id'] ?>"
- class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>"
- data-column-id="<?= $column['id'] ?>"
- data-task-limit="<?= $column['task_limit'] ?>"
- >
- <?php foreach ($column['tasks'] as $task): ?>
- <div class="task-board draggable-item task-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>"
- data-task-id="<?= $task['id'] ?>"
- data-owner-id="<?= $task['owner_id'] ?>"
- data-category-id="<?= $task['category_id'] ?>"
- data-due-date="<?= $task['date_due'] ?>"
- title="<?= t('View this task') ?>">
-
- <?= Helper\template('board_task', array('task' => $task, 'categories' => $categories)) ?>
-
- </div>
- <?php endforeach ?>
- </td>
- <?php endforeach ?>
-</tr>
-</table>
diff --git a/app/Templates/board_task.php b/app/Templates/board_task.php
deleted file mode 100644
index ca854f37..00000000
--- a/app/Templates/board_task.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php if (isset($not_editable)): ?>
-
- <a href="?controller=task&amp;action=readonly&amp;task_id=<?= $task['id'] ?>&amp;token=<?= $project['token'] ?>">#<?= $task['id'] ?></a>
-
- <?php if ($task['reference']): ?>
- <span class="task-board-reference" title="<?= t('Reference') ?>">
- (<?= $task['reference'] ?>)
- </span>
- <?php endif ?>
-
- &nbsp;-&nbsp;
-
- <span class="task-board-user">
- <?php if (! empty($task['owner_id'])): ?>
- <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?>
- <?php else: ?>
- <span class="task-board-nobody"><?= t('Nobody assigned') ?></span>
- <?php endif ?>
- </span>
-
- <?php if ($task['score']): ?>
- <span class="task-score"><?= Helper\escape($task['score']) ?></span>
- <?php endif ?>
-
- <div class="task-board-title">
- <a href="?controller=task&amp;action=readonly&amp;task_id=<?= $task['id'] ?>&amp;token=<?= $project['token'] ?>">
- <?= Helper\escape($task['title']) ?>
- </a>
- </div>
-
-<?php else: ?>
-
- <a class="task-edit-popover" href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>" title="<?= t('Edit this task') ?>">#<?= $task['id'] ?></a>
-
- <?php if ($task['reference']): ?>
- <span class="task-board-reference" title="<?= t('Reference') ?>">
- (<?= $task['reference'] ?>)
- </span>
- <?php endif ?>
-
- &nbsp;-&nbsp;
-
- <span class="task-board-user">
- <a class="assignee-popover" href="?controller=board&amp;action=changeAssignee&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>">
- <?php if (! empty($task['owner_id'])): ?>
- <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></a>
- <?php else: ?>
- <?= t('Nobody assigned') ?>
- <?php endif ?>
- </a>
- </span>
-
- <?php if ($task['score']): ?>
- <span class="task-score"><?= Helper\escape($task['score']) ?></span>
- <?php endif ?>
-
- <div class="task-board-title">
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a>
- </div>
-
-<?php endif ?>
-
-
-<?php if ($task['category_id']): ?>
-<div class="task-board-category-container">
- <span class="task-board-category">
- <a class="category-popover" href="?controller=board&amp;action=changeCategory&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change category') ?>">
- <?= Helper\in_list($task['category_id'], $categories) ?>
- </a>
- </span>
-</div>
-<?php endif ?>
-
-
-<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description']) || ! empty($task['nb_subtasks'])): ?>
-<div class="task-board-footer">
-
- <?php if (! empty($task['date_due'])): ?>
- <div class="task-board-date <?= time() > $task['date_due'] ? 'task-board-date-overdue' : '' ?>">
- <?= dt('%B %e, %Y', $task['date_due']) ?>
- </div>
- <?php endif ?>
-
- <div class="task-board-icons">
-
- <?php if (! empty($task['nb_subtasks'])): ?>
- <span title="<?= t('Sub-Tasks') ?>"><?= $task['nb_completed_subtasks'].'/'.$task['nb_subtasks'] ?> <i class="fa fa-bars"></i></span>
- <?php endif ?>
-
- <?php if (! empty($task['nb_files'])): ?>
- <span title="<?= t('Attachments') ?>"><?= $task['nb_files'] ?> <i class="fa fa-paperclip"></i></span>
- <?php endif ?>
-
- <?php if (! empty($task['nb_comments'])): ?>
- <span title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"><?= $task['nb_comments'] ?> <i class="fa fa-comment-o"></i></span>
- <?php endif ?>
-
- <?php if (! empty($task['description'])): ?>
- <span title="<?= t('Description') ?>">
- <?php if (! isset($not_editable)): ?>
- <a class="task-description-popover" href="?controller=task&amp;action=description&amp;task_id=<?= $task['id'] ?>"><i class="fa fa-file-text-o" data-href="?controller=task&amp;action=description&amp;task_id=<?= $task['id'] ?>"></i></a>
- <?php else: ?>
- <i class="fa fa-file-text-o"></i>
- <?php endif ?>
- </span>
- <?php endif ?>
- </div>
-</div>
-<?php endif ?>
diff --git a/app/Templates/category_edit.php b/app/Templates/category_edit.php
deleted file mode 100644
index 278d7e12..00000000
--- a/app/Templates/category_edit.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<div class="page-header">
- <h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2>
-</div>
-
-<form method="post" action="?controller=category&amp;action=update&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_hidden('project_id', $values) ?>
-
- <?= Helper\form_label(t('Category Name'), 'name') ?>
- <?= Helper\form_text('name', $values, $errors, array('autofocus required')) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/category_index.php b/app/Templates/category_index.php
deleted file mode 100644
index 4635406e..00000000
--- a/app/Templates/category_index.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<div class="page-header">
- <h2><?= t('Categories') ?></h2>
-</div>
-
-<?php if (! empty($categories)): ?>
-<table>
- <tr>
- <th><?= t('Category Name') ?></th>
- <th><?= t('Actions') ?></th>
- </tr>
- <?php foreach ($categories as $category_id => $category_name): ?>
- <tr>
- <td><?= Helper\escape($category_name) ?></td>
- <td>
- <ul>
- <li>
- <a href="?controller=category&amp;action=edit&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category_id ?>"><?= t('Edit') ?></a>
- </li>
- <li>
- <a href="?controller=category&amp;action=confirm&amp;project_id=<?= $project['id'] ?>&amp;category_id=<?= $category_id ?>"><?= t('Remove') ?></a>
- </li>
- </ul>
- </td>
- </tr>
- <?php endforeach ?>
-</table>
-<?php endif ?>
-
-<h3><?= t('Add a new category') ?></h3>
-<form method="post" action="?controller=category&amp;action=save&amp;project_id=<?= $project['id'] ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('project_id', $values) ?>
-
- <?= Helper\form_label(t('Category Name'), 'name') ?>
- <?= Helper\form_text('name', $values, $errors, array('autofocus required')) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/comment_create.php b/app/Templates/comment_create.php
deleted file mode 100644
index 11772f75..00000000
--- a/app/Templates/comment_create.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<div class="page-header">
- <h2><?= t('Add a comment') ?></h2>
-</div>
-
-<form method="post" action="?controller=comment&amp;action=save&amp;task_id=<?= $task['id'] ?>" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('task_id', $values) ?>
- <?= Helper\form_hidden('user_id', $values) ?>
- <?= Helper\form_textarea('comment', $values, $errors, array(! isset($skip_cancel) ? 'autofocus' : '', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/>
- <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?php if (! isset($skip_cancel)): ?>
- <?= t('or') ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- <?php endif ?>
- </div>
-</form>
diff --git a/app/Templates/comment_edit.php b/app/Templates/comment_edit.php
deleted file mode 100644
index 4ce48964..00000000
--- a/app/Templates/comment_edit.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<div class="page-header">
- <h2><?= t('Edit a comment') ?></h2>
-</div>
-
-<form method="post" action="?controller=comment&amp;action=update&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_hidden('task_id', $values) ?>
- <?= Helper\form_textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</form>
diff --git a/app/Templates/comment_forbidden.php b/app/Templates/comment_forbidden.php
deleted file mode 100644
index eeea8404..00000000
--- a/app/Templates/comment_forbidden.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<section id="main">
- <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>
-</section> \ No newline at end of file
diff --git a/app/Templates/comment_remove.php b/app/Templates/comment_remove.php
deleted file mode 100644
index 7b117781..00000000
--- a/app/Templates/comment_remove.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<div class="page-header">
- <h2><?= t('Remove a comment') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info">
- <?= t('Do you really want to remove this comment?') ?>
- </p>
-
- <?= Helper\template('comment_show', array('comment' => $comment, 'task' => $task, 'preview' => true)) ?>
-
- <div class="form-actions">
- <a href="?controller=comment&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>#comment-<?= $comment['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/comment_show.php b/app/Templates/comment_show.php
deleted file mode 100644
index b2ccc25a..00000000
--- a/app/Templates/comment_show.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>">
-
- <p class="comment-title">
- <span class="comment-username"><?= Helper\escape($comment['name'] ?: $comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %Y at %k:%M %p', $comment['date']) ?></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) && (Helper\is_admin() || Helper\is_current_user($comment['user_id']))): ?>
- <li>
- <a href="?controller=comment&amp;action=confirm&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>"><?= t('remove') ?></a>
- </li>
- <li>
- <a href="?controller=comment&amp;action=edit&amp;task_id=<?= $task['id'] ?>&amp;comment_id=<?= $comment['id'] ?>"><?= t('edit') ?></a>
- </li>
- <?php endif ?>
- </ul>
- <?php endif ?>
-
- <div class="markdown">
- <?php if (isset($is_public) && $is_public): ?>
- <?= Helper\markdown(
- $comment['comment'],
- array(
- 'controller' => 'task',
- 'action' => 'readonly',
- 'params' => array(
- 'token' => $project['token']
- )
- )
- ) ?>
- <?php else: ?>
- <?= Helper\markdown($comment['comment']) ?>
- <?php endif ?>
- </div>
-
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/config_about.php b/app/Templates/config_about.php
deleted file mode 100644
index 3f34f802..00000000
--- a/app/Templates/config_about.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<div class="page-header">
- <h2><?= t('About') ?></h2>
-</div>
-<section class="listing">
- <ul>
- <li>
- <?= t('Official website:') ?>
- <a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a>
- </li>
- <li>
- <?= t('Application version:') ?>
- <strong><?= APP_VERSION ?></strong>
- </li>
- </ul>
-</section>
-
-<div class="page-header">
- <h2><?= t('Database') ?></h2>
-</div>
-<section class="listing">
- <ul>
- <li>
- <?= t('Database driver:') ?>
- <strong><?= Helper\escape(DB_DRIVER) ?></strong>
- </li>
- <?php if (DB_DRIVER === 'sqlite'): ?>
- <li>
- <?= t('Database size:') ?>
- <strong><?= Helper\format_bytes($db_size) ?></strong>
- </li>
- <li>
- <?= Helper\a(t('Download the database'), 'config', 'downloadDb', array(), true) ?>&nbsp;
- <?= t('(Gzip compressed Sqlite file)') ?>
- </li>
- <li>
- <?= Helper\a(t('Optimize the database'), 'config', 'optimizeDb', array(), true) ?>&nbsp;
- <?= t('(VACUUM command)') ?>
- </li>
- <?php endif ?>
- </ul>
-</section> \ No newline at end of file
diff --git a/app/Templates/config_api.php b/app/Templates/config_api.php
deleted file mode 100644
index 037ea08d..00000000
--- a/app/Templates/config_api.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<div class="page-header">
- <h2><?= t('API') ?></h2>
-</div>
-<section class="listing">
- <ul>
- <li>
- <?= t('API token:') ?>
- <strong><?= Helper\escape($values['api_token']) ?></strong>
- </li>
- <li>
- <?= t('API endpoint:') ?>
- <input type="text" readonly="readonly" value="<?= Helper\get_current_base_url().'jsonrpc.php' ?>">
- </li>
- <li>
- <?= Helper\a(t('Reset token'), 'config', 'token', array('type' => 'api'), true) ?>
- </li>
- </ul>
-</section> \ No newline at end of file
diff --git a/app/Templates/config_application.php b/app/Templates/config_application.php
deleted file mode 100644
index 97071bd0..00000000
--- a/app/Templates/config_application.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<div class="page-header">
- <h2><?= t('Application settings') ?></h2>
-</div>
-<section>
-<form method="post" action="<?= Helper\u('config', 'application') ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_label(t('Application URL'), 'application_url') ?>
- <?= Helper\form_text('application_url', $values, $errors, array('placeholder="http://example.kanboar.net/"')) ?><br/>
- <p class="form-help"><?= t('Example: http://example.kanboard.net/ (used by email notifications)') ?></p>
-
- <?= Helper\form_label(t('Language'), 'application_language') ?>
- <?= Helper\form_select('application_language', $languages, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Timezone'), 'application_timezone') ?>
- <?= Helper\form_select('application_timezone', $timezones, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Date format'), 'application_date_format') ?>
- <?= Helper\form_select('application_date_format', $date_formats, $values, $errors) ?><br/>
- <p class="form-help"><?= t('ISO format is always accepted, example: "%s" and "%s"', date('Y-m-d'), date('Y_m_d')) ?></p>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
-</section> \ No newline at end of file
diff --git a/app/Templates/config_board.php b/app/Templates/config_board.php
deleted file mode 100644
index f260d084..00000000
--- a/app/Templates/config_board.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<div class="page-header">
- <h2><?= t('Board settings') ?></h2>
-</div>
-<section>
-<form method="post" action="<?= Helper\u('config', 'board') ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_label(t('Task highlight period'), 'board_highlight_period') ?>
- <?= Helper\form_number('board_highlight_period', $values, $errors) ?><br/>
- <p class="form-help"><?= t('Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)') ?></p>
-
- <?= Helper\form_label(t('Refresh interval for public board'), 'board_public_refresh_interval') ?>
- <?= Helper\form_number('board_public_refresh_interval', $values, $errors) ?><br/>
- <p class="form-help"><?= t('Frequency in second (60 seconds by default)') ?></p>
-
- <?= Helper\form_label(t('Refresh interval for private board'), 'board_private_refresh_interval') ?>
- <?= Helper\form_number('board_private_refresh_interval', $values, $errors) ?><br/>
- <p class="form-help"><?= t('Frequency in second (0 to disable this feature, 10 seconds by default)') ?></p>
-
- <?= Helper\form_label(t('Default columns for new projects (Comma-separated)'), 'board_columns') ?>
- <?= Helper\form_text('board_columns', $values, $errors) ?><br/>
- <p class="form-help"><?= t('Default values are "%s"', $default_columns) ?></p>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
-</section> \ No newline at end of file
diff --git a/app/Templates/config_layout.php b/app/Templates/config_layout.php
deleted file mode 100644
index 3aacb9b7..00000000
--- a/app/Templates/config_layout.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('Settings') ?></h2>
- </div>
- <section class="config-show" id="config-section">
-
- <?= Helper\template('config_sidebar') ?>
-
- <div class="config-show-main">
- <?= $config_content_for_layout ?>
- </div>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/config_sidebar.php b/app/Templates/config_sidebar.php
deleted file mode 100644
index d96159b8..00000000
--- a/app/Templates/config_sidebar.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<div class="config-show-sidebar">
- <h2><?= t('Actions') ?></h2>
- <div class="config-show-actions">
- <ul>
- <li>
- <?= Helper\a(t('About'), 'config', 'index') ?>
- </li>
- <li>
- <?= Helper\a(t('Application settings'), 'config', 'application') ?>
- </li>
- <li>
- <?= Helper\a(t('Board settings'), 'config', 'board') ?>
- </li>
- <li>
- <?= Helper\a(t('Webhooks'), 'config', 'webhook') ?>
- </li>
- <li>
- <?= Helper\a(t('API'), 'config', 'api') ?>
- </li>
- </ul>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/config_webhook.php b/app/Templates/config_webhook.php
deleted file mode 100644
index 052a2a99..00000000
--- a/app/Templates/config_webhook.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<div class="page-header">
- <h2><?= t('Webhook settings') ?></h2>
-</div>
-<section>
-<form method="post" action="<?= Helper\u('config', 'webhook') ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_label(t('Webhook URL for task creation'), 'webhook_url_task_creation') ?>
- <?= Helper\form_text('webhook_url_task_creation', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Webhook URL for task modification'), 'webhook_url_task_modification') ?>
- <?= Helper\form_text('webhook_url_task_modification', $values, $errors) ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form>
-</section>
-
-<div class="page-header">
- <h2><?= t('URL and token') ?></h2>
-</div>
-<section class="listing">
- <ul>
- <li>
- <?= t('Webhook token:') ?>
- <strong><?= Helper\escape($values['webhook_token']) ?></strong>
- </li>
- <li>
- <?= t('URL for task creation:') ?>
- <input type="text" readonly="readonly" value="<?= Helper\get_current_base_url().Helper\u('webhook', 'task', array('token' => $values['webhook_token'])) ?>">
- </li>
- <li>
- <?= Helper\a(t('Reset token'), 'config', 'token', array('type' => 'webhook'), true) ?>
- </li>
- </ul>
-</section> \ No newline at end of file
diff --git a/app/Templates/event_comment_create.php b/app/Templates/event_comment_create.php
deleted file mode 100644
index d2f6f97b..00000000
--- a/app/Templates/event_comment_create.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<p class="activity-title">
- <?= e('%s commented the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
-</p>
-<div class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em><br/>
- <div class="markdown"><?= Helper\markdown($comment['comment']) ?></div>
-</div> \ No newline at end of file
diff --git a/app/Templates/event_comment_update.php b/app/Templates/event_comment_update.php
deleted file mode 100644
index 27cc0be6..00000000
--- a/app/Templates/event_comment_update.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<p class="activity-title">
- <?= e('%s updated a comment on the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
-</p>
-<div class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em><br/>
- <div class="markdown"><?= Helper\markdown($comment['comment']) ?></div>
-</div> \ No newline at end of file
diff --git a/app/Templates/event_task_assignee_change.php b/app/Templates/event_task_assignee_change.php
deleted file mode 100644
index b346325e..00000000
--- a/app/Templates/event_task_assignee_change.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<p class="activity-title">
- <?= e(
- '%s change the assignee of the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to %s',
- Helper\escape($author),
- $task_id,
- $task_id,
- Helper\escape($task['assignee_name'] ?: $task['assignee_username'])
- ) ?>
-</p>
-<p class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em>
-</p> \ No newline at end of file
diff --git a/app/Templates/event_task_close.php b/app/Templates/event_task_close.php
deleted file mode 100644
index 48d25678..00000000
--- a/app/Templates/event_task_close.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<p class="activity-title">
- <?= e('%s closed the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
-</p>
-<p class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em>
-</p> \ No newline at end of file
diff --git a/app/Templates/event_task_create.php b/app/Templates/event_task_create.php
deleted file mode 100644
index 2515af05..00000000
--- a/app/Templates/event_task_create.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<p class="activity-title">
- <?= e('%s created the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
-</p>
-<p class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em>
-</p> \ No newline at end of file
diff --git a/app/Templates/event_task_move_column.php b/app/Templates/event_task_move_column.php
deleted file mode 100644
index f2aac8f7..00000000
--- a/app/Templates/event_task_move_column.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<p class="activity-title">
- <?= e('%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the column "%s"', Helper\escape($author), $task_id, $task_id, Helper\escape($task['column_title'])) ?>
-</p>
-<p class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em>
-</p> \ No newline at end of file
diff --git a/app/Templates/event_task_move_position.php b/app/Templates/event_task_move_position.php
deleted file mode 100644
index 26cdeb13..00000000
--- a/app/Templates/event_task_move_position.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<p class="activity-title">
- <?= e('%s moved the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a> to the position #%d in the column "%s"', Helper\escape($author), $task_id, $task_id, $task['position'], Helper\escape($task['column_title'])) ?>
-</p>
-<p class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em>
-</p> \ No newline at end of file
diff --git a/app/Templates/event_task_open.php b/app/Templates/event_task_open.php
deleted file mode 100644
index 9623be74..00000000
--- a/app/Templates/event_task_open.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<p class="activity-title">
- <?= e('%s open the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
-</p>
-<p class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em>
-</p> \ No newline at end of file
diff --git a/app/Templates/event_task_update.php b/app/Templates/event_task_update.php
deleted file mode 100644
index a270b936..00000000
--- a/app/Templates/event_task_update.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<p class="activity-title">
- <?= e('%s updated the task <a href="?controller=task&amp;action=show&amp;task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?>
-</p>
-<p class="activity-description">
- <em><?= Helper\escape($task['title']) ?></em>
-</p> \ No newline at end of file
diff --git a/app/Templates/file_new.php b/app/Templates/file_new.php
deleted file mode 100644
index 7f7f1d1c..00000000
--- a/app/Templates/file_new.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="page-header">
- <h2><?= t('Attach a document') ?></h2>
-</div>
-
-<form action="?controller=file&amp;action=save&amp;task_id=<?= $task['id'] ?>" method="post" enctype="multipart/form-data">
- <?= Helper\form_csrf() ?>
- <input type="file" name="files[]" multiple />
- <div class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? Helper\format_bytes($max_size) : $max_size ?></div>
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/file_open.php b/app/Templates/file_open.php
deleted file mode 100644
index aa181d64..00000000
--- a/app/Templates/file_open.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<div class="page-header">
- <h2><?= Helper\escape($file['name']) ?></h2>
- <div class="task-file-viewer">
- <img src="?controller=file&amp;action=image&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $file['task_id'] ?>" alt="<?= Helper\escape($file['name']) ?>"/>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/file_remove.php b/app/Templates/file_remove.php
deleted file mode 100644
index af77591c..00000000
--- a/app/Templates/file_remove.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<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"?', Helper\escape($file['name'])) ?>
- </p>
-
- <div class="form-actions">
- <a href="?controller=file&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;file_id=<?= $file['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/file_show.php b/app/Templates/file_show.php
deleted file mode 100644
index 3832a0f5..00000000
--- a/app/Templates/file_show.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php if (! empty($files)): ?>
-<div id="attachments" class="task-show-section">
-
- <div class="page-header">
- <h2><?= t('Attachments') ?></h2>
- </div>
-
- <ul class="task-show-files">
- <?php foreach ($files as $file): ?>
- <li>
- <a href="?controller=file&amp;action=download&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a>
- <span class="task-show-file-actions">
- <?php if ($file['is_image']): ?>
- <a href="?controller=file&amp;action=open&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>" class="file-popover"><?= t('open') ?></a>,
- <?php endif ?>
- <a href="?controller=file&amp;action=confirm&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('remove') ?></a>
- </span>
- </li>
- <?php endforeach ?>
- </ul>
-
-</div>
-<?php endif ?> \ No newline at end of file
diff --git a/app/Templates/layout.php b/app/Templates/layout.php
deleted file mode 100644
index a86d613b..00000000
--- a/app/Templates/layout.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width">
- <meta name="mobile-web-app-capable" content="yes">
- <meta name="robots" content="noindex,nofollow">
-
- <?php if (isset($board_public_refresh_interval)): ?>
- <meta http-equiv="refresh" content="<?= $board_public_refresh_interval ?>" >
- <?php endif ?>
-
- <?php if (! isset($not_editable)): ?>
- <?= Helper\js('assets/js/jquery-1.11.1.min.js') ?>
- <?= Helper\js('assets/js/jquery-ui-1.10.4.custom.min.js') ?>
- <?= Helper\js('assets/js/jquery.ui.touch-punch.min.js') ?>
- <?= Helper\js('assets/js/chosen.jquery.min.js') ?>
- <?= Helper\js('assets/js/app.js') ?>
- <?php endif ?>
-
- <?= Helper\css('assets/css/app.css') ?>
- <?= Helper\css('assets/css/font-awesome.min.css') ?>
- <?= Helper\css('assets/css/jquery-ui-1.10.4.custom.css'); ?>
- <?= Helper\css('assets/css/chosen.min.css'); ?>
-
- <link rel="icon" type="image/png" href="assets/img/favicon.png">
- <link rel="apple-touch-icon" href="assets/img/touch-icon-iphone.png">
- <link rel="apple-touch-icon" sizes="72x72" href="assets/img/touch-icon-ipad.png">
- <link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png">
- <link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png">
-
- <title><?= isset($title) ? Helper\escape($title).' - Kanboard' : 'Kanboard' ?></title>
- </head>
- <body>
- <?php if (isset($no_layout) && $no_layout): ?>
- <?= $content_for_layout ?>
- <?php else: ?>
- <header>
- <nav>
- <a class="logo" href="?">kanboard</a>
-
- <ul>
- <?php if (isset($board_selector) && ! empty($board_selector)): ?>
- <li>
- <select id="board-selector" data-placeholder="<?= t('Display another project') ?>">
- <option value=""></option>
- <?php foreach($board_selector as $board_id => $board_name): ?>
- <option value="<?= $board_id ?>"><?= Helper\escape($board_name) ?></option>
- <?php endforeach ?>
- </select>
- </li>
- <?php endif ?>
- <li <?= isset($menu) && $menu === 'dashboard' ? 'class="active"' : '' ?>>
- <a href="?controller=app"><?= t('Dashboard') ?></a>
- </li>
- <li <?= isset($menu) && $menu === 'boards' ? 'class="active"' : '' ?>>
- <a href="?controller=board"><?= t('Boards') ?></a>
- </li>
- <li <?= isset($menu) && $menu === 'projects' ? 'class="active"' : '' ?>>
- <a href="?controller=project"><?= t('Projects') ?></a>
- </li>
- <?php if (Helper\is_admin()): ?>
- <li <?= isset($menu) && $menu === 'users' ? 'class="active"' : '' ?>>
- <a href="?controller=user"><?= t('Users') ?></a>
- </li>
- <li class="hide-tablet <?= isset($menu) && $menu === 'config' ? 'active' : '' ?>">
- <a href="?controller=config"><?= t('Settings') ?></a>
- </li>
- <?php endif ?>
- <li>
- <a href="?controller=user&amp;action=logout<?= Helper\param_csrf() ?>"><?= t('Logout') ?></a>
- <span class="username">(<a href="?controller=user&amp;action=show&amp;user_id=<?= Helper\get_user_id() ?>"><?= Helper\escape(Helper\get_username()) ?></a>)</span>
- </li>
- </ul>
- </nav>
- </header>
- <section class="page">
- <?= Helper\flash('<div class="alert alert-success alert-fade-out">%s</div>') ?>
- <?= Helper\flash_error('<div class="alert alert-error">%s</div>') ?>
- <?= $content_for_layout ?>
- </section>
- <?php endif ?>
- </body>
-</html>
diff --git a/app/Templates/notification_comment_creation.php b/app/Templates/notification_comment_creation.php
deleted file mode 100644
index 5b334d76..00000000
--- a/app/Templates/notification_comment_creation.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<h3><?= t('New comment posted by %s', $comment['name'] ?: $comment['username']) ?></h3>
-
-<?= Helper\markdown($comment['comment']) ?>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_comment_update.php b/app/Templates/notification_comment_update.php
deleted file mode 100644
index 04aafb85..00000000
--- a/app/Templates/notification_comment_update.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<h3><?= t('Comment updated') ?></h3>
-
-<?= Helper\markdown($comment['comment']) ?>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_file_creation.php b/app/Templates/notification_file_creation.php
deleted file mode 100644
index d8636820..00000000
--- a/app/Templates/notification_file_creation.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<h3><?= t('New attachment added "%s"', $file['name']) ?></h3>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_footer.php b/app/Templates/notification_footer.php
deleted file mode 100644
index 533621f4..00000000
--- a/app/Templates/notification_footer.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<hr/>
-Kanboard
-
-<?php if ($application_url): ?>
- - <a href="<?= $application_url.'?controller=task&action=show&task_id='.$task['id'] ?>"><?= t('view the task on Kanboard') ?></a>.
-<?php endif ?>
diff --git a/app/Templates/notification_subtask_creation.php b/app/Templates/notification_subtask_creation.php
deleted file mode 100644
index 2ddfc649..00000000
--- a/app/Templates/notification_subtask_creation.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<h3><?= t('New sub-task') ?></h3>
-
-<ul>
- <li><?= t('Title:') ?> <?= Helper\escape($subtask['title']) ?></li>
- <li><?= t('Status:') ?> <?= Helper\escape($subtask['status_name']) ?></li>
- <li><?= t('Assignee:') ?> <?= Helper\escape($subtask['name'] ?: $subtask['username'] ?: '?') ?></li>
- <li>
- <?= t('Time tracking:') ?>
- <?php if (! empty($subtask['time_estimated'])): ?>
- <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
- <?php endif ?>
- </li>
-</ul>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_subtask_update.php b/app/Templates/notification_subtask_update.php
deleted file mode 100644
index 999edbf9..00000000
--- a/app/Templates/notification_subtask_update.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<h3><?= t('Sub-task updated') ?></h3>
-
-<ul>
- <li><?= t('Title:') ?> <?= Helper\escape($subtask['title']) ?></li>
- <li><?= t('Status:') ?> <?= Helper\escape($subtask['status_name']) ?></li>
- <li><?= t('Assignee:') ?> <?= Helper\escape($subtask['name'] ?: $subtask['username'] ?: '?') ?></li>
- <li>
- <?= t('Time tracking:') ?>
- <?php if (! empty($subtask['time_spent'])): ?>
- <strong><?= Helper\escape($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?>
- <?php endif ?>
-
- <?php if (! empty($subtask['time_estimated'])): ?>
- <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
- <?php endif ?>
- </li>
-</ul>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_task_close.php b/app/Templates/notification_task_close.php
deleted file mode 100644
index d56e71bb..00000000
--- a/app/Templates/notification_task_close.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<p><?= t('The task #%d have been closed.', $task['id']) ?></p>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_task_due.php b/app/Templates/notification_task_due.php
deleted file mode 100644
index ae02f64e..00000000
--- a/app/Templates/notification_task_due.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<h2><?= t('List of due tasks for the project "%s"', $project) ?></h2>
-
-<ul>
- <?php foreach ($tasks as $task): ?>
- <li>
- (<strong>#<?= $task['id'] ?></strong>)
- <?= Helper\escape($task['title']) ?>
- <?php if ($task['assignee_username']): ?>
- (<strong><?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></strong>)
- <?php endif ?>
- </li>
- <?php endforeach ?>
-</ul>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_task_move_column.php b/app/Templates/notification_task_move_column.php
deleted file mode 100644
index c3f94df7..00000000
--- a/app/Templates/notification_task_move_column.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<ul>
- <li>
- <?= t('Column on the board:') ?>
- <strong><?= Helper\escape($task['column_title']) ?></strong>
- </li>
- <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li>
-</ul>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_task_move_position.php b/app/Templates/notification_task_move_position.php
deleted file mode 100644
index c3f94df7..00000000
--- a/app/Templates/notification_task_move_position.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<ul>
- <li>
- <?= t('Column on the board:') ?>
- <strong><?= Helper\escape($task['column_title']) ?></strong>
- </li>
- <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li>
-</ul>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/notification_task_open.php b/app/Templates/notification_task_open.php
deleted file mode 100644
index 5d9f7d5b..00000000
--- a/app/Templates/notification_task_open.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2>
-
-<p><?= t('The task #%d have been opened.', $task['id']) ?></p>
-
-<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file
diff --git a/app/Templates/project_activity.php b/app/Templates/project_activity.php
deleted file mode 100644
index d07ba86a..00000000
--- a/app/Templates/project_activity.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('%s\'s activity', $project['name']) ?></h2>
- <ul>
- <li><?= Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?></li>
- <li><?= Helper\a(t('Search'), 'project', 'search', array('project_id' => $project['id'])) ?></li>
- <li><?= Helper\a(t('Completed tasks'), 'project', 'tasks', array('project_id' => $project['id'])) ?></li>
- <li><?= Helper\a(t('List of projects'), 'project', 'index') ?></li>
- </ul>
- </div>
- <section>
- <?php if ($project['is_public']): ?>
- <p class="pull-right"><i class="fa fa-rss-square"></i> <?= Helper\a(t('RSS feed'), 'project', 'feed', array('token' => $project['token'])) ?></p>
- <?php endif ?>
-
- <?= Helper\template('project_events', array('events' => $events)) ?>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/project_duplicate.php b/app/Templates/project_duplicate.php
deleted file mode 100644
index a926dcd1..00000000
--- a/app/Templates/project_duplicate.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="page-header">
- <h2><?= t('Clone this project') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info">
- <?= t('Do you really want to duplicate this project: "%s"?', $project['name']) ?>
- </p>
-
- <div class="form-actions">
- <?= Helper\a(t('Yes'), 'project', 'duplicate', array('project_id' => $project['id'], 'duplicate' => 'yes'), true, 'btn btn-red') ?>
- <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/project_edit.php b/app/Templates/project_edit.php
deleted file mode 100644
index 8eb2110d..00000000
--- a/app/Templates/project_edit.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="page-header">
- <h2><?= t('Edit project') ?></h2>
-</div>
-<form method="post" action="<?= Helper\u('project', 'update', array('project_id' => $values['id'])) ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('id', $values) ?>
-
- <?= Helper\form_label(t('Name'), 'name') ?>
- <?= Helper\form_text('name', $values, $errors, array('required')) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/project_export.php b/app/Templates/project_export.php
deleted file mode 100644
index 02eb389f..00000000
--- a/app/Templates/project_export.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<div class="page-header">
- <h2>
- <?= t('Tasks exportation for "%s"', $project['name']) ?>
- </h2>
-</div>
-
-<form method="get" action="?" autocomplete="off">
-
- <?= Helper\form_hidden('controller', $values) ?>
- <?= Helper\form_hidden('action', $values) ?>
- <?= Helper\form_hidden('project_id', $values) ?>
-
- <?= Helper\form_label(t('Start Date'), 'from') ?>
- <?= Helper\form_text('from', $values, $errors, array('required', 'placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?><br/>
-
- <?= Helper\form_label(t('End Date'), 'to') ?>
- <?= Helper\form_text('to', $values, $errors, array('required', 'placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?>
-
- <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"/>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/project_index.php b/app/Templates/project_index.php
deleted file mode 100644
index b575e958..00000000
--- a/app/Templates/project_index.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('Projects') ?><span id="page-counter"> (<?= $nb_projects ?>)</span></h2>
- <ul>
- <?php if (Helper\is_admin()): ?>
- <li><?= Helper\a(t('New project'), 'project', 'create') ?></li>
- <?php endif ?>
- <li><?= Helper\a(t('New private project'), 'project', 'create', array('private' => 1)) ?></li>
- </ul>
- </div>
- <section>
- <?php if (empty($active_projects) && empty($inactive_projects)): ?>
- <p class="alert"><?= t('No project') ?></p>
- <?php else: ?>
-
- <?php if (! empty($active_projects)): ?>
- <h3><?= t('Active projects') ?></h3>
- <ul class="project-listing">
- <?php foreach ($active_projects as $project): ?>
- <li>
- <?php if ($project['is_public']): ?>
- <i class="fa fa-share-alt fa-fw"></i>
- <?php endif ?>
- <?php if ($project['is_private']): ?>
- <i class="fa fa-lock fa-fw"></i>
- <?php endif ?>
- <?= Helper\a(Helper\escape($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php endforeach ?>
- </ul>
- <?php endif ?>
-
- <?php if (! empty($inactive_projects)): ?>
- <h3><?= t('Inactive projects') ?></h3>
- <ul class="project-listing">
- <?php foreach ($inactive_projects as $project): ?>
- <li>
- <?php if ($project['is_private']): ?>
- <i class="fa fa-lock"></i>
- <?php endif ?>
- <?= Helper\a(Helper\escape($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php endforeach ?>
- </ul>
- <?php endif ?>
-
- <?php endif ?>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/project_layout.php b/app/Templates/project_layout.php
deleted file mode 100644
index d69bbd53..00000000
--- a/app/Templates/project_layout.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('Project "%s"', $project['name']) ?> (#<?= $project['id'] ?>)</h2>
- <ul>
- <li><?= Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?></li>
- <li><?= Helper\a(t('All projects'), 'project', 'index') ?></li>
- </ul>
- </div>
- <section class="project-show" id="project-section">
-
- <?= Helper\template('project_sidebar', array('project' => $project)) ?>
-
- <div class="project-show-main">
- <?= $project_content_for_layout ?>
- </div>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/project_new.php b/app/Templates/project_new.php
deleted file mode 100644
index e1ea5af7..00000000
--- a/app/Templates/project_new.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= empty($values['is_private']) ? t('New project') : t('New private project') ?></h2>
- <ul>
- <li><?= Helper\a(t('All projects'), 'project', 'index') ?></li>
- </ul>
- </div>
- <section>
- <form method="post" action="<?= Helper\u('project', 'save') ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('is_private', $values) ?>
- <?= Helper\form_label(t('Name'), 'name') ?>
- <?= Helper\form_text('name', $values, $errors, array('autofocus', 'required')) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'index') ?>
- </div>
- </form>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/project_search.php b/app/Templates/project_search.php
deleted file mode 100644
index 7d5d8795..00000000
--- a/app/Templates/project_search.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2>
- <?= t('Search in the project "%s"', $project['name']) ?>
- <?php if (! empty($nb_tasks)): ?>
- <span id="page-counter"> (<?= $nb_tasks ?>)</span>
- <?php endif ?>
- </h2>
- <ul>
- <li><?= Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?></li>
- <li><?= Helper\a(t('Completed tasks'), 'project', 'tasks', array('project_id' => $project['id'])) ?></li>
- <li><?= Helper\a(t('Activity'), 'project', 'activity', array('project_id' => $project['id'])) ?></li>
- <li><?= Helper\a(t('List of projects'), 'project', 'index') ?></li>
- </ul>
- </div>
- <section>
- <form method="get" action="?" autocomplete="off">
- <?= Helper\form_hidden('controller', $values) ?>
- <?= Helper\form_hidden('action', $values) ?>
- <?= Helper\form_hidden('project_id', $values) ?>
- <?= Helper\form_text('search', $values, array(), array('autofocus', 'required', 'placeholder="'.t('Search').'"')) ?>
- <input type="submit" value="<?= t('Search') ?>" class="btn btn-blue"/>
- </form>
-
- <?php if (empty($tasks) && ! empty($values['search'])): ?>
- <p class="alert"><?= t('Nothing found.') ?></p>
- <?php elseif (! empty($tasks)): ?>
- <?= Helper\template('task_table', array(
- 'tasks' => $tasks,
- 'categories' => $categories,
- 'columns' => $columns,
- 'pagination' => $pagination,
- )) ?>
- <?php endif ?>
-
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/project_share.php b/app/Templates/project_share.php
deleted file mode 100644
index 8edcbbc0..00000000
--- a/app/Templates/project_share.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<div class="page-header">
- <h2><?= t('Public access') ?></h2>
-</div>
-
-<?php if ($project['is_public']): ?>
-
- <div class="listing">
- <ul class="no-bullet">
- <li><strong><i class="fa fa-share-alt"></i> <?= Helper\a(t('Public link'), 'board', 'readonly', array('token' => $project['token'])) ?></strong></li>
- <li><strong><i class="fa fa-rss-square"></i> <?= Helper\a(t('RSS feed'), 'project', 'feed', array('token' => $project['token'])) ?></strong></li>
- </ul>
- <input type="text" readonly="readonly" value="<?= Helper\get_current_base_url().Helper\u('board', 'readonly', array('token' => $project['token'])) ?>"/>
- </div>
-
- <?= Helper\a(t('Disable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'disable'), true, 'btn btn-red') ?>
-
-<?php else: ?>
- <?= Helper\a(t('Enable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?>
-<?php endif ?>
diff --git a/app/Templates/project_show.php b/app/Templates/project_show.php
deleted file mode 100644
index facdc60a..00000000
--- a/app/Templates/project_show.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<div class="page-header">
- <h2><?= t('Summary') ?></h2>
-</div>
-<ul class="listing">
- <li><strong><?= $project['is_active'] ? t('Active') : t('Inactive') ?></strong></li>
-
- <?php if ($project['is_private']): ?>
- <li><i class="fa fa-lock"></i> <?= t('This project is private') ?></li>
- <?php endif ?>
-
- <?php if ($project['is_public']): ?>
- <li><i class="fa fa-share-alt"></i> <?= Helper\a(t('Public link'), 'board', 'readonly', array('token' => $project['token'])) ?></li>
- <li><i class="fa fa-rss-square"></i> <?= Helper\a(t('RSS feed'), 'project', 'feed', array('token' => $project['token'])) ?></li>
- <?php else: ?>
- <li><?= t('Public access disabled') ?></li>
- <?php endif ?>
-
- <?php if ($project['last_modified']): ?>
- <li><?= dt('Last modified on %B %e, %Y at %k:%M %p', $project['last_modified']) ?></li>
- <?php endif ?>
-
- <?php if ($stats['nb_tasks'] > 0): ?>
-
- <?php if ($stats['nb_active_tasks'] > 0): ?>
- <li><?= Helper\a(t('%d tasks on the board', $stats['nb_active_tasks']), 'board', 'show', array('project_id' => $project['id'])) ?></li>
- <?php endif ?>
-
- <?php if ($stats['nb_inactive_tasks'] > 0): ?>
- <li><?= Helper\a(t('%d closed tasks', $stats['nb_inactive_tasks']), 'project', 'tasks', array('project_id' => $project['id'])) ?></li>
- <?php endif ?>
-
- <li><?= t('%d tasks in total', $stats['nb_tasks']) ?></li>
-
- <?php else: ?>
- <li><?= t('No task for this project') ?></li>
- <?php endif ?>
-</ul>
-
-<div class="page-header">
- <h2><?= t('Board') ?></h2>
-</div>
-<table class="table-stripped">
- <tr>
- <th width="50%"><?= t('Column') ?></th>
- <th><?= t('Task limit') ?></th>
- <th><?= t('Active tasks') ?></th>
- </tr>
- <?php foreach ($stats['columns'] as $column): ?>
- <tr>
- <td><?= Helper\escape($column['title']) ?></td>
- <td><?= $column['task_limit'] ?: '∞' ?></td>
- <td><?= $column['nb_active_tasks'] ?></td>
- </tr>
- <?php endforeach ?>
-</table>
diff --git a/app/Templates/project_sidebar.php b/app/Templates/project_sidebar.php
deleted file mode 100644
index 7bad1f0e..00000000
--- a/app/Templates/project_sidebar.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<div class="project-show-sidebar">
- <h2><?= t('Actions') ?></h2>
- <div class="project-show-actions">
- <ul>
- <li>
- <a href="?controller=project&amp;action=show&amp;project_id=<?= $project['id'] ?>"><?= t('Summary') ?></a>
- </li>
-
- <?php if (Helper\is_admin() || $project['is_private']): ?>
- <li>
- <a href="?controller=project&amp;action=export&amp;project_id=<?= $project['id'] ?>"><?= t('Tasks Export') ?></a>
- </li>
- <li>
- <a href="?controller=project&amp;action=share&amp;project_id=<?= $project['id'] ?>"><?= t('Public access') ?></a>
- </li>
- <li>
- <a href="?controller=project&amp;action=edit&amp;project_id=<?= $project['id'] ?>"><?= t('Edit project') ?></a>
- </li>
- <li>
- <a href="?controller=board&amp;action=edit&amp;project_id=<?= $project['id'] ?>"><?= t('Edit board') ?></a>
- </li>
- <li>
- <a href="?controller=category&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('Category management') ?></a>
- </li>
- <?php if (Helper\is_admin()): ?>
- <li>
- <a href="?controller=project&amp;action=users&amp;project_id=<?= $project['id'] ?>"><?= t('User management') ?></a>
- </li>
- <?php endif ?>
- <li>
- <a href="?controller=action&amp;action=index&amp;project_id=<?= $project['id'] ?>"><?= t('Automatic actions') ?></a>
- </li>
- <li>
- <a href="?controller=project&amp;action=duplicate&amp;project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Duplicate') ?></a>
- </li>
- <li>
- <?php if ($project['is_active']): ?>
- <a href="?controller=project&amp;action=disable&amp;project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Disable') ?></a>
- <?php else: ?>
- <a href="?controller=project&amp;action=enable&amp;project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Enable') ?></a>
- <?php endif ?>
- </li>
- <li>
- <a href="?controller=project&amp;action=remove&amp;project_id=<?= $project['id'] ?>"><?= t('Remove') ?></a>
- </li>
- <?php endif ?>
- </ul>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/project_tasks.php b/app/Templates/project_tasks.php
deleted file mode 100644
index 7b6f2d9c..00000000
--- a/app/Templates/project_tasks.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('Completed tasks for "%s"', $project['name']) ?><span id="page-counter"> (<?= $nb_tasks ?>)</span></h2>
- <ul>
- <li><a href="?controller=board&amp;action=show&amp;project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li>
- <li><a href="?controller=project&amp;action=search&amp;project_id=<?= $project['id'] ?>"><?= t('Search') ?></a></li>
- <li><a href="?controller=project&amp;action=activity&amp;project_id=<?= $project['id'] ?>"><?= t('Activity') ?></a></li>
- <li><a href="?controller=project&amp;action=index"><?= t('List of projects') ?></a></li>
- </ul>
- </div>
- <section>
- <?php if (empty($tasks)): ?>
- <p class="alert"><?= t('No task') ?></p>
- <?php else: ?>
- <?= Helper\template('task_table', array(
- 'tasks' => $tasks,
- 'categories' => $categories,
- 'columns' => $columns,
- 'pagination' => $pagination,
- )) ?>
- <?php endif ?>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/project_users.php b/app/Templates/project_users.php
deleted file mode 100644
index 35079df6..00000000
--- a/app/Templates/project_users.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<div class="page-header">
- <h2><?= t('List of authorized users') ?></h2>
-</div>
-
-<?php if ($project['is_everybody_allowed']): ?>
- <div class="alert alert-info"><?= 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: ?>
- <div class="listing">
- <p><?= t('Only those users have access to this project:') ?></p>
- <ul>
- <?php foreach ($users['allowed'] as $user_id => $username): ?>
- <li>
- <strong><?= Helper\escape($username) ?></strong>
- <?php if ($project['is_private'] == 0): ?>
- (<?= Helper\a(t('revoke'), 'project', 'revoke', array('project_id' => $project['id'], 'user_id' => $user_id), true) ?>)
- <?php endif ?>
- </li>
- <?php endforeach ?>
- </ul>
- <p><?= t('Don\'t forget that administrators have access to everything.') ?></p>
- </div>
- <?php endif ?>
-
- <?php if ($project['is_private'] == 0 && ! empty($users['not_allowed'])): ?>
- <form method="post" action="<?= Helper\u('project', 'allow', array('project_id' => $project['id'])) ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_hidden('project_id', array('project_id' => $project['id'])) ?>
-
- <?= Helper\form_label(t('User'), 'user_id') ?>
- <?= Helper\form_select('user_id', $users['not_allowed']) ?><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): ?>
-<form method="post" action="<?= Helper\u('project', 'allowEverybody', array('project_id' => $project['id'])) ?>">
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_hidden('id', array('id' => $project['id'])) ?>
- <?= Helper\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/Templates/subtask_create.php b/app/Templates/subtask_create.php
deleted file mode 100644
index c8ee556b..00000000
--- a/app/Templates/subtask_create.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<div class="page-header">
- <h2><?= t('Add a sub-task') ?></h2>
-</div>
-
-<form method="post" action="?controller=subtask&amp;action=save&amp;task_id=<?= $task['id'] ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_hidden('task_id', $values) ?>
-
- <?= Helper\form_label(t('Title'), 'title') ?>
- <?= Helper\form_text('title', $values, $errors, array('required autofocus')) ?><br/>
-
- <?= Helper\form_label(t('Assignee'), 'user_id') ?>
- <?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?>
- <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
-
- <?= Helper\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"/>
- <?= t('or') ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</form>
diff --git a/app/Templates/subtask_edit.php b/app/Templates/subtask_edit.php
deleted file mode 100644
index 91690d0a..00000000
--- a/app/Templates/subtask_edit.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<div class="page-header">
- <h2><?= t('Edit a sub-task') ?></h2>
-</div>
-
-<form method="post" action="?controller=subtask&amp;action=update&amp;task_id=<?= $task['id'] ?>&amp;subtask_id=<?= $subtask['id'] ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_hidden('task_id', $values) ?>
-
- <?= Helper\form_label(t('Title'), 'title') ?>
- <?= Helper\form_text('title', $values, $errors, array('required autofocus')) ?><br/>
-
- <?= Helper\form_label(t('Status'), 'status') ?>
- <?= Helper\form_select('status', $status_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Assignee'), 'user_id') ?>
- <?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?>
- <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
-
- <?= Helper\form_label(t('Time spent'), 'time_spent') ?>
- <?= Helper\form_numeric('time_spent', $values, $errors) ?> <?= t('hours') ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</form>
diff --git a/app/Templates/subtask_remove.php b/app/Templates/subtask_remove.php
deleted file mode 100644
index 12c99cf1..00000000
--- a/app/Templates/subtask_remove.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<div class="page-header">
- <h2><?= t('Remove a sub-task') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info">
- <?= t('Do you really want to remove this sub-task?') ?>
- </p>
-
- <p><strong><?= Helper\escape($subtask['title']) ?></strong></p>
-
- <div class="form-actions">
- <a href="?controller=subtask&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;subtask_id=<?= $subtask['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>#subtasks"><?= t('cancel') ?></a>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/subtask_show.php b/app/Templates/subtask_show.php
deleted file mode 100644
index f1b0466f..00000000
--- a/app/Templates/subtask_show.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php if (! empty($subtasks)): ?>
-<div id="subtasks" class="task-show-section">
-
- <div class="page-header">
- <h2><?= t('Sub-Tasks') ?></h2>
- </div>
-
- <table class="subtasks-table">
- <tr>
- <th width="40%"><?= t('Title') ?></th>
- <th><?= t('Status') ?></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><?= Helper\escape($subtask['title']) ?></td>
- <td>
- <?php if (!isset($not_editable)): ?>
- <a href="<?= Helper\u('subtask', 'toggleStatus', array('task_id' => $task['id'], 'subtask_id' => $subtask['id'])) ?>">
- <?php endif ?>
- <?php if ($subtask['status'] == 0): ?>
- <i class="fa fa-square-o fa-fw"></i><i class="fa">&nbsp;<?= Helper\escape($subtask['status_name']) ?></i>
- <?php elseif ($subtask['status'] == 1): ?>
- <i class="fa fa-gears fa-fw"></i><i class="fa">&nbsp;<?= Helper\escape($subtask['status_name']) ?></i>
- <?php else: ?>
- <i class="fa fa-check-square-o fa-fw"></i><i class="fa">&nbsp;<?= Helper\escape($subtask['status_name']) ?></i>
- <?php endif ?>
- <?php if (! isset($not_editable)): ?>
- </a>
- <?php endif ?>
- </td>
-
- <td>
- <?php if (! empty($subtask['username'])): ?>
- <?= Helper\escape($subtask['name'] ?: $subtask['username']) ?>
- <?php endif ?>
- </td>
- <td>
- <?php if (! empty($subtask['time_spent'])): ?>
- <strong><?= Helper\escape($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?>
- <?php endif ?>
-
- <?php if (! empty($subtask['time_estimated'])): ?>
- <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
- <?php endif ?>
- </td>
- <?php if (! isset($not_editable)): ?>
- <td>
- <?= Helper\a(t('Edit'), 'subtask', 'edit', array('task_id' => $task['id'], 'subtask_id' => $subtask['id'])) ?>
- <?= t('or') ?>
- <?= Helper\a(t('Remove'), 'subtask', 'confirm', array('task_id' => $task['id'], 'subtask_id' => $subtask['id'])) ?>
- </td>
- <?php endif ?>
- </tr>
- <?php endforeach ?>
- </table>
-
- <?php if (! isset($not_editable)): ?>
- <form method="post" action="<?= Helper\u('subtask', 'save', array('task_id' => $task['id'])) ?>" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('task_id', array('task_id' => $task['id'])) ?>
- <?= Helper\form_text('title', array(), array(), array('required', 'placeholder="'.t('Type here to create a new sub-task').'"')) ?>
- <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/>
- </form>
- <?php endif ?>
-
-</div>
-<?php endif ?>
diff --git a/app/Templates/task_close.php b/app/Templates/task_close.php
deleted file mode 100644
index 2abfd032..00000000
--- a/app/Templates/task_close.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="page-header">
- <h2><?= t('Close a task') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info">
- <?= t('Do you really want to close this task: "%s"?', Helper\escape($task['title'])) ?>
- </p>
-
- <div class="form-actions">
- <a href="?controller=task&amp;action=close&amp;confirmation=yes&amp;task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/task_duplicate.php b/app/Templates/task_duplicate.php
deleted file mode 100644
index ef903f1d..00000000
--- a/app/Templates/task_duplicate.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="page-header">
- <h2><?= t('Duplicate a task') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info">
- <?= t('Do you really want to duplicate this task?') ?>
- </p>
-
- <div class="form-actions">
- <a href="?controller=task&amp;action=duplicate&amp;confirmation=yes&amp;task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/task_duplicate_project.php b/app/Templates/task_duplicate_project.php
deleted file mode 100644
index 86d2114a..00000000
--- a/app/Templates/task_duplicate_project.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<div class="page-header">
- <h2><?= t('Duplicate the task to another project') ?></h2>
-</div>
-
-<?php if (empty($projects_list)): ?>
- <p class="alert"><?= t('No project') ?></p>
-<?php else: ?>
-
- <form method="post" action="?controller=task&amp;action=copy&amp;task_id=<?= $task['id'] ?>&amp;project_id=<?= $task['project_id'] ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_label(t('Project'), 'project_id') ?>
- <?= Helper\form_select('project_id', $projects_list, $values, $errors) ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
- </form>
-
-<?php endif ?> \ No newline at end of file
diff --git a/app/Templates/task_edit.php b/app/Templates/task_edit.php
deleted file mode 100644
index 73e00a31..00000000
--- a/app/Templates/task_edit.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<div class="page-header">
- <h2><?= t('Edit a task') ?></h2>
-</div>
-<section id="task-section">
-<form method="post" action="?controller=task&amp;action=update&amp;task_id=<?= $task['id'] ?>&amp;ajax=<?= $ajax ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <div class="form-column">
-
- <?= Helper\form_label(t('Title'), 'title') ?>
- <?= Helper\form_text('title', $values, $errors, array('required')) ?><br/>
-
- <?= Helper\form_label(t('Description'), 'description') ?>
- <?= Helper\form_textarea('description', $values, $errors) ?><br/>
- <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
-
- </div>
-
- <div class="form-column">
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_hidden('project_id', $values) ?>
-
- <?= Helper\form_label(t('Assignee'), 'owner_id') ?>
- <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Category'), 'category_id') ?>
- <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Color'), 'color_id') ?>
- <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Complexity'), 'score') ?>
- <?= Helper\form_number('score', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Due Date'), 'date_due') ?>
- <?= Helper\form_text('date_due', $values, $errors, array('placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?><br/>
- <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
- </div>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <?php if ($ajax): ?>
- <a href="?controller=board&amp;action=show&amp;project_id=<?= $task['project_id'] ?>"><?= t('cancel') ?></a>
- <?php else: ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- <?php endif ?>
- </div>
-</form>
-</section>
diff --git a/app/Templates/task_edit_description.php b/app/Templates/task_edit_description.php
deleted file mode 100644
index 2d2a4d0b..00000000
--- a/app/Templates/task_edit_description.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<div class="page-header">
- <h2><?= t('Edit the description') ?></h2>
-</div>
-
-<form method="post" action="?controller=task&amp;action=description&amp;task_id=<?= $task['id'] ?>&amp;ajax=<?= $ajax ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_textarea('description', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a description').'"'), 'description-textarea') ?><br/>
- <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <?php if ($ajax): ?>
- <a href="?controller=board&amp;action=show&amp;project_id=<?= $task['project_id'] ?>"><?= t('cancel') ?></a>
- <?php else: ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- <?php endif ?>
- </div>
-</form>
diff --git a/app/Templates/task_layout.php b/app/Templates/task_layout.php
deleted file mode 100644
index ca0a413f..00000000
--- a/app/Templates/task_layout.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= Helper\escape($task['project_name']) ?> &gt; <?= t('Task #%d', $task['id']) ?></h2>
- <ul>
- <li><a href="?controller=board&amp;action=show&amp;project_id=<?= $task['project_id'] ?>"><?= t('Back to the board') ?></a></li>
- </ul>
- </div>
- <section class="task-show" id="task-section">
-
- <?= Helper\template('task_sidebar', array('task' => $task, 'hide_remove_menu' => isset($hide_remove_menu))) ?>
-
- <div class="task-show-main">
- <?= $task_content_for_layout ?>
- </div>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/task_move_project.php b/app/Templates/task_move_project.php
deleted file mode 100644
index 3bc3bcb8..00000000
--- a/app/Templates/task_move_project.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<div class="page-header">
- <h2><?= t('Move the task to another project') ?></h2>
-</div>
-
-<?php if (empty($projects_list)): ?>
- <p class="alert"><?= t('No project') ?></p>
-<?php else: ?>
-
- <form method="post" action="?controller=task&amp;action=move&amp;task_id=<?= $task['id'] ?>&amp;project_id=<?= $task['project_id'] ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_label(t('Project'), 'project_id') ?>
- <?= Helper\form_select('project_id', $projects_list, $values, $errors) ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
- </form>
-
-<?php endif ?> \ No newline at end of file
diff --git a/app/Templates/task_new.php b/app/Templates/task_new.php
deleted file mode 100644
index 51142165..00000000
--- a/app/Templates/task_new.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('New task') ?></h2>
- </div>
- <section id="task-section">
- <form method="post" action="?controller=task&amp;action=save" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <div class="form-column">
- <?= Helper\form_label(t('Title'), 'title') ?>
- <?= Helper\form_text('title', $values, $errors, array('autofocus', 'required')) ?><br/>
-
- <?= Helper\form_label(t('Description'), 'description') ?>
- <?= Helper\form_textarea('description', $values, $errors) ?><br/>
- <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div>
-
- <?php if (! isset($duplicate)): ?>
- <?= Helper\form_checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?>
- <?php endif ?>
- </div>
-
- <div class="form-column">
- <?= Helper\form_hidden('project_id', $values) ?>
-
- <?= Helper\form_label(t('Assignee'), 'owner_id') ?>
- <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Category'), 'category_id') ?>
- <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Column'), 'column_id') ?>
- <?= Helper\form_select('column_id', $columns_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Color'), 'color_id') ?>
- <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Complexity'), 'score') ?>
- <?= Helper\form_number('score', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?>
- <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
-
- <?= Helper\form_label(t('Due Date'), 'date_due') ?>
- <?= Helper\form_text('date_due', $values, $errors, array('placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?><br/>
- <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div>
- </div>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <a href="?controller=board&amp;action=show&amp;project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a>
- </div>
- </form>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/task_open.php b/app/Templates/task_open.php
deleted file mode 100644
index d28970e3..00000000
--- a/app/Templates/task_open.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="page-header">
- <h2><?= t('Open a task') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info">
- <?= t('Do you really want to open this task: "%s"?', Helper\escape($task['title'])) ?>
- </p>
-
- <div class="form-actions">
- <a href="?controller=task&amp;action=open&amp;confirmation=yes&amp;task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/task_public.php b/app/Templates/task_public.php
deleted file mode 100644
index 13fef1ed..00000000
--- a/app/Templates/task_public.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<section id="main" class="public-task">
-
- <?= Helper\template('task_details', array('task' => $task, 'project' => $project)) ?>
-
- <p class="pull-right"><?= Helper\a(t('Back to the board'), 'board', 'readonly', array('token' => $project['token'])) ?></p>
-
- <?= Helper\template('task_show_description', array(
- 'task' => $task,
- 'project' => $project,
- 'is_public' => true
- )) ?>
-
- <?= Helper\template('subtask_show', array(
- 'task' => $task,
- 'subtasks' => $subtasks,
- 'not_editable' => true
- )) ?>
-
- <?= Helper\template('task_comments', array(
- 'task' => $task,
- 'comments' => $comments,
- 'project' => $project,
- 'not_editable' => true,
- 'is_public' => true,
- )) ?>
-
-</section> \ No newline at end of file
diff --git a/app/Templates/task_remove.php b/app/Templates/task_remove.php
deleted file mode 100644
index 496ac2d8..00000000
--- a/app/Templates/task_remove.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="page-header">
- <h2><?= t('Remove a task') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info">
- <?= t('Do you really want to remove this task: "%s"?', Helper\escape($task['title'])) ?>
- </p>
-
- <div class="form-actions">
- <a href="?controller=task&amp;action=remove&amp;confirmation=yes&amp;task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/task_show.php b/app/Templates/task_show.php
deleted file mode 100644
index ec5d5da4..00000000
--- a/app/Templates/task_show.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?= Helper\template('task_details', array('task' => $task, 'project' => $project)) ?>
-<?= Helper\template('task_time', array('values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?>
-<?= Helper\template('task_show_description', array('task' => $task)) ?>
-<?= Helper\template('subtask_show', array('task' => $task, 'subtasks' => $subtasks)) ?>
-<?= Helper\template('task_timesheet', array('timesheet' => $timesheet)) ?>
-<?= Helper\template('file_show', array('task' => $task, 'files' => $files)) ?>
-<?= Helper\template('task_comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?> \ No newline at end of file
diff --git a/app/Templates/task_sidebar.php b/app/Templates/task_sidebar.php
deleted file mode 100644
index 4cffd5fa..00000000
--- a/app/Templates/task_sidebar.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<div class="task-show-sidebar">
- <h2><?= t('Actions') ?></h2>
- <div class="task-show-actions">
- <ul>
- <li><a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('Summary') ?></a></li>
- <li><a href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>"><?= t('Edit the task') ?></a></li>
- <li><a href="?controller=task&amp;action=description&amp;task_id=<?= $task['id'] ?>"><?= t('Edit the description') ?></a></li>
- <li><a href="?controller=subtask&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Add a sub-task') ?></a></li>
- <li><a href="?controller=comment&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Add a comment') ?></a></li>
- <li><a href="?controller=file&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Attach a document') ?></a></li>
- <li><a href="?controller=task&amp;action=duplicate&amp;project_id=<?= $task['project_id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('Duplicate') ?></a></li>
- <li><a href="?controller=task&amp;action=copy&amp;project_id=<?= $task['project_id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('Duplicate to another project') ?></a></li>
- <li><a href="?controller=task&amp;action=move&amp;project_id=<?= $task['project_id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('Move to another project') ?></a></li>
- <li>
- <?php if ($task['is_active'] == 1): ?>
- <a href="?controller=task&amp;action=close&amp;task_id=<?= $task['id'] ?>"><?= t('Close this task') ?></a>
- <?php else: ?>
- <a href="?controller=task&amp;action=open&amp;task_id=<?= $task['id'] ?>"><?= t('Open this task') ?></a>
- <?php endif ?>
- </li>
- <?php if (! $hide_remove_menu): ?>
- <li><a href="?controller=task&amp;action=remove&amp;task_id=<?= $task['id'] ?>"><?= t('Remove') ?></a></li>
- <?php endif ?>
- </ul>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/task_table.php b/app/Templates/task_table.php
deleted file mode 100644
index fa04fa55..00000000
--- a/app/Templates/task_table.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<table>
- <tr>
- <th><?= Helper\order(t('Id'), 'tasks.id', $pagination) ?></th>
- <th><?= Helper\order(t('Column'), 'tasks.column_id', $pagination) ?></th>
- <th><?= Helper\order(t('Category'), 'tasks.category_id', $pagination) ?></th>
- <th><?= Helper\order(t('Title'), 'tasks.title', $pagination) ?></th>
- <th><?= Helper\order(t('Assignee'), 'users.username', $pagination) ?></th>
- <th><?= Helper\order(t('Due date'), 'tasks.date_due', $pagination) ?></th>
- <th><?= Helper\order(t('Date created'), 'tasks.date_creation', $pagination) ?></th>
- <th><?= Helper\order(t('Date completed'), 'tasks.date_completed', $pagination) ?></th>
- <th><?= Helper\order(t('Status'), 'tasks.is_active', $pagination) ?></th>
- </tr>
- <?php foreach ($tasks as $task): ?>
- <tr>
- <td class="task-table task-<?= $task['color_id'] ?>">
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>">#<?= Helper\escape($task['id']) ?></a>
- </td>
- <td>
- <?= Helper\in_list($task['column_id'], $columns) ?>
- </td>
- <td>
- <?= Helper\in_list($task['category_id'], $categories, '') ?>
- </td>
- <td>
- <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a>
- </td>
- <td>
- <?php if ($task['assignee_username']): ?>
- <?= Helper\escape($task['assignee_name'] ?: $task['assignee_username']) ?>
- <?php else: ?>
- <?= t('Unassigned') ?>
- <?php endif ?>
- </td>
- <td>
- <?= dt('%B %e, %Y', $task['date_due']) ?>
- </td>
- <td>
- <?= dt('%B %e, %Y at %k:%M %p', $task['date_creation']) ?>
- </td>
- <td>
- <?php if ($task['date_completed']): ?>
- <?= dt('%B %e, %Y at %k:%M %p', $task['date_completed']) ?>
- <?php endif ?>
- </td>
- <td>
- <?php if ($task['is_active'] == \Model\Task::STATUS_OPEN): ?>
- <?= t('Open') ?>
- <?php else: ?>
- <?= t('Closed') ?>
- <?php endif ?>
- </td>
- </tr>
- <?php endforeach ?>
-</table>
-
-<?= Helper\paginate($pagination) ?>
diff --git a/app/Templates/task_time.php b/app/Templates/task_time.php
deleted file mode 100644
index 11a76303..00000000
--- a/app/Templates/task_time.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<form method="post" action="<?= Helper\u('task', 'time', array('task_id' => $values['id'])) ?>" class="form-inline task-time-form" autocomplete="off">
- <?= Helper\form_csrf() ?>
- <?= Helper\form_hidden('id', $values) ?>
-
- <?= Helper\form_label(t('Start date'), 'date_started') ?>
- <?= Helper\form_text('date_started', $values, array(), array('placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?>
-
- <?= Helper\form_label(t('Time estimated'), 'time_estimated') ?>
- <?= Helper\form_numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?>
-
- <?= Helper\form_label(t('Time spent'), 'time_spent') ?>
- <?= Helper\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/Templates/task_timesheet.php b/app/Templates/task_timesheet.php
deleted file mode 100644
index cd093657..00000000
--- a/app/Templates/task_timesheet.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php if ($timesheet['time_estimated'] > 0 || $timesheet['time_spent'] > 0): ?>
-
-<div class="page-header">
- <h2><?= t('Time tracking') ?></h2>
-</div>
-
-<ul class="listing">
- <li><?= t('Estimate:') ?> <strong><?= Helper\escape($timesheet['time_estimated']) ?></strong> <?= t('hours') ?></li>
- <li><?= t('Spent:') ?> <strong><?= Helper\escape($timesheet['time_spent']) ?></strong> <?= t('hours') ?></li>
- <li><?= t('Remaining:') ?> <strong><?= Helper\escape($timesheet['time_remaining']) ?></strong> <?= t('hours') ?></li>
-</ul>
-
-<?php endif ?> \ No newline at end of file
diff --git a/app/Templates/user_edit.php b/app/Templates/user_edit.php
deleted file mode 100644
index 14063d49..00000000
--- a/app/Templates/user_edit.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<div class="page-header">
- <h2><?= t('Edit user') ?></h2>
-</div>
-<form method="post" action="?controller=user&amp;action=edit&amp;user_id=<?= $user['id'] ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_hidden('is_ldap_user', $values) ?>
-
- <?= Helper\form_label(t('Username'), 'username') ?>
- <?= Helper\form_text('username', $values, $errors, array('required', $values['is_ldap_user'] == 1 ? 'readonly' : '')) ?><br/>
-
- <?= Helper\form_label(t('Name'), 'name') ?>
- <?= Helper\form_text('name', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Email'), 'email') ?>
- <?= Helper\form_email('email', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Default project'), 'default_project_id') ?>
- <?= Helper\form_select('default_project_id', $projects, $values, $errors) ?><br/>
-
- <?php if (Helper\is_admin()): ?>
- <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?><br/>
- <?php endif ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <?= t('or') ?> <a href="?controller=user&amp;action=show&amp;user_id=<?= $user['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/user_index.php b/app/Templates/user_index.php
deleted file mode 100644
index d4e1bbf9..00000000
--- a/app/Templates/user_index.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('Users') ?><span id="page-counter"> (<?= $nb_users ?>)</span></h2>
- <?php if (Helper\is_admin()): ?>
- <ul>
- <li><a href="?controller=user&amp;action=create"><?= t('New user') ?></a></li>
- </ul>
- <?php endif ?>
- </div>
- <section>
- <?php if (empty($users)): ?>
- <p class="alert"><?= t('No user') ?></p>
- <?php else: ?>
- <table>
- <tr>
- <th><?= t('Id') ?></th>
- <th><?= t('Username') ?></th>
- <th><?= t('Name') ?></th>
- <th><?= t('Email') ?></th>
- <th><?= t('Administrator') ?></th>
- <th><?= t('Default project') ?></th>
- <th><?= t('Notifications') ?></th>
- <th><?= t('External accounts') ?></th>
- <th><?= t('Account type') ?></th>
- </tr>
- <?php foreach ($users as $user): ?>
- <tr>
- <td>
- <a href="?controller=user&amp;action=show&amp;user_id=<?= $user['id'] ?>">#<?= $user['id'] ?></a>
- </td>
- <td>
- <a href="?controller=user&amp;action=show&amp;user_id=<?= $user['id'] ?>"><?= Helper\escape($user['username']) ?></a>
- </td>
- <td>
- <?= Helper\escape($user['name']) ?>
- </td>
- <td>
- <?= Helper\escape($user['email']) ?>
- </td>
- <td>
- <?= $user['is_admin'] ? t('Yes') : t('No') ?>
- </td>
- <td>
- <?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? Helper\escape($projects[$user['default_project_id']]) : t('None'); ?>
- </td>
- <td>
- <?php if ($user['notifications_enabled'] == 1): ?>
- <?= t('Enabled') ?>
- <?php else: ?>
- <?= t('Disabled') ?>
- <?php endif ?>
- </td>
- <td>
- <ul class="no-bullet">
- <?php if ($user['google_id']): ?>
- <li><i class="fa fa-google"></i> <?= t('Google account linked') ?></li>
- <?php endif ?>
- <?php if ($user['github_id']): ?>
- <li><i class="fa fa-github"></i> <?= t('Github account linked') ?></li>
- <?php endif ?>
- </ul>
- </td>
- <td>
- <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?>
- </td>
- </tr>
- <?php endforeach ?>
- </table>
- <?php endif ?>
- </section>
-</section>
diff --git a/app/Templates/user_layout.php b/app/Templates/user_layout.php
deleted file mode 100644
index 7462b3f0..00000000
--- a/app/Templates/user_layout.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= Helper\escape($user['name'] ?: $user['username']).' (#'.$user['id'].')' ?></h2>
- <?php if (Helper\is_admin()): ?>
- <ul>
- <li><a href="?controller=user&amp;action=index"><?= t('All users') ?></a></li>
- <li><a href="?controller=user&amp;action=create"><?= t('New user') ?></a></li>
- </ul>
- <?php endif ?>
- </div>
- <section class="user-show" id="user-section">
-
- <?= Helper\template('user_sidebar', array('user' => $user)) ?>
-
- <div class="user-show-main">
- <?= $user_content_for_layout ?>
- </div>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/user_login.php b/app/Templates/user_login.php
deleted file mode 100644
index cf92cd4d..00000000
--- a/app/Templates/user_login.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<div class="form-login">
-
- <div class="page-header">
- <h1><?= t('Sign in') ?></h1>
- </div>
-
- <?php if (isset($errors['login'])): ?>
- <p class="alert alert-error"><?= Helper\escape($errors['login']) ?></p>
- <?php endif ?>
-
- <form method="post" action="?controller=user&amp;action=check&amp;redirect_query=<?= urlencode($redirect_query) ?>">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_label(t('Username'), 'username') ?>
- <?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/>
-
- <?= Helper\form_label(t('Password'), 'password') ?>
- <?= Helper\form_password('password', $values, $errors, array('required')) ?>
-
- <?= Helper\form_checkbox('remember_me', t('Remember Me'), 1) ?><br/>
-
- <ul>
- <?php if (GOOGLE_AUTH): ?>
- <li>
- <a href="?controller=user&amp;action=google"><?= t('Login with my Google Account') ?></a>
- </li>
- <?php endif ?>
-
- <?php if (GITHUB_AUTH): ?>
- <li>
- <a href="?controller=user&amp;action=gitHub"><?= t('Login with my GitHub Account') ?></a>
- </li>
- <?php endif ?>
- </ul>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
- </div>
- </form>
-
-</div> \ No newline at end of file
diff --git a/app/Templates/user_new.php b/app/Templates/user_new.php
deleted file mode 100644
index 158813cb..00000000
--- a/app/Templates/user_new.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<section id="main">
- <div class="page-header">
- <h2><?= t('New user') ?></h2>
- <ul>
- <li><a href="?controller=user"><?= t('All users') ?></a></li>
- </ul>
- </div>
- <section>
- <form method="post" action="?controller=user&amp;action=save" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_label(t('Username'), 'username') ?>
- <?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/>
-
- <?= Helper\form_label(t('Name'), 'name') ?>
- <?= Helper\form_text('name', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Email'), 'email') ?>
- <?= Helper\form_email('email', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Password'), 'password') ?>
- <?= Helper\form_password('password', $values, $errors, array('required')) ?><br/>
-
- <?= Helper\form_label(t('Confirmation'), 'confirmation') ?>
- <?= Helper\form_password('confirmation', $values, $errors, array('required')) ?><br/>
-
- <?= Helper\form_label(t('Default project'), 'default_project_id') ?>
- <?= Helper\form_select('default_project_id', $projects, $values, $errors) ?><br/>
-
- <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a>
- </div>
- </form>
- </section>
-</section> \ No newline at end of file
diff --git a/app/Templates/user_notifications.php b/app/Templates/user_notifications.php
deleted file mode 100644
index 13dd9809..00000000
--- a/app/Templates/user_notifications.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<div class="page-header">
- <h2><?= t('Email notifications') ?></h2>
-</div>
-
-<form method="post" action="?controller=user&amp;action=notifications&amp;user_id=<?= $user['id'] ?>" autocomplete="off">
-
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_checkbox('notifications_enabled', t('Enable email notifications'), '1', $notifications['notifications_enabled'] == 1) ?><br/>
-
- <p><?= t('I want to receive notifications only for those projects:') ?><br/><br/></p>
-
- <div class="form-checkbox-group">
- <?php foreach ($projects as $project_id => $project_name): ?>
- <?= Helper\form_checkbox('projects['.$project_id.']', $project_name, '1', isset($notifications['project_'.$project_id])) ?>
- <?php endforeach ?>
- </div>
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
- <?= t('or') ?> <a href="?controller=user&amp;action=show&amp;user_id=<?= $user['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</form> \ No newline at end of file
diff --git a/app/Templates/user_password.php b/app/Templates/user_password.php
deleted file mode 100644
index 5da38595..00000000
--- a/app/Templates/user_password.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<div class="page-header">
- <h2><?= t('Password modification') ?></h2>
-</div>
-
-<form method="post" action="?controller=user&amp;action=password&amp;user_id=<?= $user['id'] ?>" autocomplete="off">
-
- <?= Helper\form_hidden('id', $values) ?>
- <?= Helper\form_csrf() ?>
-
- <?= Helper\form_label(t('Current password for the user "%s"', Helper\get_username()), 'current_password') ?>
- <?= Helper\form_password('current_password', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('New password for the user "%s"', Helper\get_username($user)), 'password') ?>
- <?= Helper\form_password('password', $values, $errors) ?><br/>
-
- <?= Helper\form_label(t('Confirmation'), 'confirmation') ?>
- <?= Helper\form_password('confirmation', $values, $errors) ?><br/>
-
- <div class="form-actions">
- <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <?= t('or') ?> <a href="?controller=user&amp;action=show&amp;user_id=<?= $user['id'] ?>"><?= t('cancel') ?></a>
- </div>
-
-</form> \ No newline at end of file
diff --git a/app/Templates/user_remove.php b/app/Templates/user_remove.php
deleted file mode 100644
index c20ccbba..00000000
--- a/app/Templates/user_remove.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<div class="page-header">
- <h2><?= t('Remove user') ?></h2>
-</div>
-
-<div class="confirm">
- <p class="alert alert-info"><?= t('Do you really want to remove this user: "%s"?', $user['name'] ?: $user['username']) ?></p>
-
- <div class="form-actions">
- <a href="?controller=user&amp;action=remove&amp;confirmation=yes&amp;user_id=<?= $user['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a>
- <?= t('or') ?> <a href="?controller=user&amp;action=show&amp;user_id=<?= $user['id'] ?>"><?= t('cancel') ?></a>
- </div>
-</div> \ No newline at end of file
diff --git a/app/Templates/user_show.php b/app/Templates/user_show.php
deleted file mode 100644
index 1c843751..00000000
--- a/app/Templates/user_show.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<div class="page-header">
- <h2><?= t('Summary') ?></h2>
-</div>
-<ul class="listing">
- <li><?= t('Username:') ?> <strong><?= Helper\escape($user['username']) ?></strong></li>
- <li><?= t('Name:') ?> <strong><?= Helper\escape($user['name']) ?></strong></li>
- <li><?= t('Email:') ?> <strong><?= Helper\escape($user['email']) ?></strong></li>
- <li><?= t('Default project:') ?> <strong><?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? Helper\escape($projects[$user['default_project_id']]) : t('None'); ?></strong></li>
- <li><?= t('Notifications:') ?> <strong><?= $user['notifications_enabled'] == 1 ? t('Enabled') : t('Disabled') ?></strong></li>
- <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : t('Regular user') ?></strong></li>
- <li><?= t('Account type:') ?> <strong><?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?></strong></li>
-</ul>
diff --git a/app/Templates/user_sidebar.php b/app/Templates/user_sidebar.php
deleted file mode 100644
index 9d8f8b46..00000000
--- a/app/Templates/user_sidebar.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<div class="project-show-sidebar">
- <h2><?= t('Actions') ?></h2>
- <div class="user-show-actions">
- <ul>
- <li>
- <a href="?controller=user&amp;action=show&amp;user_id=<?= $user['id'] ?>"><?= t('Summary') ?></a>
- </li>
-
- <?php if (Helper\is_admin() || Helper\is_current_user($user['id'])): ?>
- <li>
- <a href="?controller=user&amp;action=edit&amp;user_id=<?= $user['id'] ?>"><?= t('Edit profile') ?></a>
- </li>
-
- <?php if ($user['is_ldap_user'] == 0): ?>
- <li>
- <a href="?controller=user&amp;action=password&amp;user_id=<?= $user['id'] ?>"><?= t('Change password') ?></a>
- </li>
- <?php endif ?>
-
- <li>
- <a href="?controller=user&amp;action=notifications&amp;user_id=<?= $user['id'] ?>"><?= t('Email notifications') ?></a>
- </li>
- <li>
- <a href="?controller=user&amp;action=external&amp;user_id=<?= $user['id'] ?>"><?= t('External accounts') ?></a>
- </li>
- <li>
- <a href="?controller=user&amp;action=last&amp;user_id=<?= $user['id'] ?>"><?= t('Last logins') ?></a>
- </li>
- <li>
- <a href="?controller=user&amp;action=sessions&amp;user_id=<?= $user['id'] ?>"><?= t('Persistent connections') ?></a>
- </li>
- <?php endif ?>
-
- <?php if (Helper\is_admin()): ?>
- <li>
- <a href="?controller=user&amp;action=remove&amp;user_id=<?= $user['id'] ?>"><?= t('Remove') ?></a>
- </li>
- <?php endif ?>
-
- </ul>
- </div>
-</div> \ No newline at end of file
diff --git a/app/check_setup.php b/app/check_setup.php
index afb08f6a..065b8e10 100644
--- a/app/check_setup.php
+++ b/app/check_setup.php
@@ -33,3 +33,8 @@ if (! extension_loaded('mbstring')) {
if (! is_writable('data')) {
die('The directory "data" must be writeable by your web server user');
}
+
+// Fix wrong value for arg_separator.output, used by the function http_build_query()
+if (ini_get('arg_separator.output') === '&amp;') {
+ ini_set('arg_separator.output', '&');
+}
diff --git a/app/common.php b/app/common.php
index 1ace3d81..f4485267 100644
--- a/app/common.php
+++ b/app/common.php
@@ -1,17 +1,18 @@
<?php
-// Common file between cli and web interface
+require 'vendor/autoload.php';
-require __DIR__.'/Core/Loader.php';
-require __DIR__.'/helpers.php';
-require __DIR__.'/functions.php';
+// Automatically parse environment configuration (Heroku)
+if (getenv('DATABASE_URL')) {
-use Core\Loader;
-use Core\Registry;
+ $dbopts = parse_url(getenv('DATABASE_URL'));
-// Include password_compat for PHP < 5.5
-if (version_compare(PHP_VERSION, '5.5.0', '<')) {
- require __DIR__.'/../vendor/password.php';
+ define('DB_DRIVER', $dbopts['scheme']);
+ define('DB_USERNAME', $dbopts["user"]);
+ define('DB_PASSWORD', $dbopts["pass"]);
+ define('DB_HOSTNAME', $dbopts["host"]);
+ define('DB_PORT', isset($dbopts["port"]) ? $dbopts["port"] : null);
+ define('DB_NAME', ltrim($dbopts["path"],'/'));
}
// Include custom config file
@@ -21,12 +22,8 @@ if (file_exists('config.php')) {
require __DIR__.'/constants.php';
-$loader = new Loader;
-$loader->setPath('app');
-$loader->setPath('vendor');
-$loader->execute();
-
-$registry = new Registry;
-$registry->db = setup_db();
-$registry->event = setup_events();
-$registry->mailer = function() { return setup_mailer(); };
+$container = new Pimple\Container;
+$container->register(new ServiceProvider\LoggingProvider);
+$container->register(new ServiceProvider\DatabaseProvider);
+$container->register(new ServiceProvider\ClassProvider);
+$container->register(new ServiceProvider\EventDispatcherProvider);
diff --git a/app/constants.php b/app/constants.php
index 93075892..0b934569 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -1,7 +1,8 @@
<?php
-// Custom session save path
-defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', '');
+// Enable/disable debug
+defined('DEBUG') or define('DEBUG', false);
+defined('DEBUG_FILE') or define('DEBUG_FILE', __DIR__.'/../data/debug.log');
// Application version
defined('APP_VERSION') or define('APP_VERSION', 'master');
@@ -20,11 +21,13 @@ defined('DB_USERNAME') or define('DB_USERNAME', 'root');
defined('DB_PASSWORD') or define('DB_PASSWORD', '');
defined('DB_HOSTNAME') or define('DB_HOSTNAME', 'localhost');
defined('DB_NAME') or define('DB_NAME', 'kanboard');
+defined('DB_PORT') or define('DB_PORT', null);
// 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_BIND_TYPE') or define('LDAP_BIND_TYPE', 'anonymous');
defined('LDAP_USERNAME') or define('LDAP_USERNAME', null);
@@ -33,6 +36,8 @@ 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_USERNAME_CASE_SENSITIVE') or define('LDAP_USERNAME_CASE_SENSITIVE', false);
// Google authentication
defined('GOOGLE_AUTH') or define('GOOGLE_AUTH', false);
@@ -51,7 +56,7 @@ defined('REVERSE_PROXY_DEFAULT_ADMIN') or define('REVERSE_PROXY_DEFAULT_ADMIN',
defined('REVERSE_PROXY_DEFAULT_DOMAIN') or define('REVERSE_PROXY_DEFAULT_DOMAIN', '');
// Mail configuration
-defined('MAIL_FROM') or define('MAIL_FROM', 'notifications@kanboard.net');
+defined('MAIL_FROM') or define('MAIL_FROM', 'notifications@kanboard.local');
defined('MAIL_TRANSPORT') or define('MAIL_TRANSPORT', 'mail');
defined('MAIL_SMTP_HOSTNAME') or define('MAIL_SMTP_HOSTNAME', '');
defined('MAIL_SMTP_PORT') or define('MAIL_SMTP_PORT', 25);
@@ -59,6 +64,21 @@ defined('MAIL_SMTP_USERNAME') or define('MAIL_SMTP_USERNAME', '');
defined('MAIL_SMTP_PASSWORD') or define('MAIL_SMTP_PASSWORD', '');
defined('MAIL_SMTP_ENCRYPTION') or define('MAIL_SMTP_ENCRYPTION', null);
defined('MAIL_SENDMAIL_COMMAND') or define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs');
+defined('POSTMARK_API_TOKEN') or define('POSTMARK_API_TOKEN', '');
+defined('MAILGUN_API_TOKEN') or define('MAILGUN_API_TOKEN', '');
+defined('MAILGUN_DOMAIN') or define('MAILGUN_DOMAIN', '');
// Enable or disable "Strict-Transport-Security" HTTP header
defined('ENABLE_HSTS') or define('ENABLE_HSTS', true);
+
+// Enable or disable "X-Frame-Options: DENY" HTTP header
+defined('ENABLE_XFRAME') or define('ENABLE_XFRAME', true);
+
+// Default files directory
+defined('FILES_DIR') or define('FILES_DIR', 'data/files/');
+
+// Escape html inside markdown text
+defined('MARKDOWN_ESCAPE_HTML') or define('MARKDOWN_ESCAPE_HTML', true);
+
+// API alternative authentication header, the default is HTTP Basic Authentication defined in RFC2617
+defined('API_AUTHENTICATION_HEADER') or define('API_AUTHENTICATION_HEADER', '');
diff --git a/app/functions.php b/app/functions.php
index c39eaf98..e4b38cdd 100644
--- a/app/functions.php
+++ b/app/functions.php
@@ -1,143 +1,6 @@
<?php
-use Core\Event;
use Core\Translator;
-use PicoDb\Database;
-
-/**
- * Send a debug message to the log files
- *
- * @param mixed $message Variable or string
- */
-function debug($message)
-{
- if (! is_string($message)) {
- $message = var_export($message, true);
- }
-
- error_log($message.PHP_EOL, 3, 'data/debug.log');
-}
-
-/**
- * Setup events
- *
- * @return Core\Event
- */
-function setup_events()
-{
- return new Event;
-}
-
-/**
- * Setup the mailer according to the configuration
- *
- * @return Swift_SmtpTransport
- */
-function setup_mailer()
-{
- require_once __DIR__.'/../vendor/swiftmailer/swift_required.php';
-
- switch (MAIL_TRANSPORT) {
- case 'smtp':
- $transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
- $transport->setUsername(MAIL_SMTP_USERNAME);
- $transport->setPassword(MAIL_SMTP_PASSWORD);
- $transport->setEncryption(MAIL_SMTP_ENCRYPTION);
- break;
- case 'sendmail':
- $transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
- break;
- default:
- $transport = Swift_MailTransport::newInstance();
- }
-
- return $transport;
-}
-
-/**
- * Setup the database driver and execute schema migration
- *
- * @return PicoDb\Database
- */
-function setup_db()
-{
- switch (DB_DRIVER) {
- case 'sqlite':
- $db = setup_sqlite();
- break;
-
- case 'mysql':
- $db = setup_mysql();
- break;
-
- case 'postgres':
- $db = setup_postgres();
- break;
-
- default:
- die('Database driver not supported');
- }
-
- if ($db->schema()->check(Schema\VERSION)) {
- return $db;
- }
- else {
- $errors = $db->getLogMessages();
- die('Unable to migrate database schema: <br/><br/><strong>'.(isset($errors[0]) ? $errors[0] : 'Unknown error').'</strong>');
- }
-}
-
-/**
- * Setup the Sqlite database driver
- *
- * @return PicoDb\Database
- */
-function setup_sqlite()
-{
- require_once __DIR__.'/Schema/Sqlite.php';
-
- return new Database(array(
- 'driver' => 'sqlite',
- 'filename' => DB_FILENAME
- ));
-}
-
-/**
- * Setup the Mysql database driver
- *
- * @return PicoDb\Database
- */
-function setup_mysql()
-{
- require_once __DIR__.'/Schema/Mysql.php';
-
- return new Database(array(
- 'driver' => 'mysql',
- 'hostname' => DB_HOSTNAME,
- 'username' => DB_USERNAME,
- 'password' => DB_PASSWORD,
- 'database' => DB_NAME,
- 'charset' => 'utf8',
- ));
-}
-
-/**
- * Setup the Postgres database driver
- *
- * @return PicoDb\Database
- */
-function setup_postgres()
-{
- require_once __DIR__.'/Schema/Postgres.php';
-
- return new Database(array(
- 'driver' => 'postgres',
- 'hostname' => DB_HOSTNAME,
- 'username' => DB_USERNAME,
- 'password' => DB_PASSWORD,
- 'database' => DB_NAME,
- ));
-}
/**
* Translate a string
@@ -146,8 +9,7 @@ function setup_postgres()
*/
function t()
{
- $t = new Translator;
- return call_user_func_array(array($t, 'translate'), func_get_args());
+ return call_user_func_array(array(Translator::getInstance(), 'translate'), func_get_args());
}
/**
@@ -157,19 +19,7 @@ function t()
*/
function e()
{
- $t = new Translator;
- return call_user_func_array(array($t, 'translateNoEscaping'), func_get_args());
-}
-
-/**
- * Translate a currency
- *
- * @return string
- */
-function c($value)
-{
- $t = new Translator;
- return $t->currency($value);
+ return call_user_func_array(array(Translator::getInstance(), 'translateNoEscaping'), func_get_args());
}
/**
@@ -179,8 +29,7 @@ function c($value)
*/
function n($value)
{
- $t = new Translator;
- return $t->number($value);
+ return Translator::getInstance()->number($value);
}
/**
@@ -190,8 +39,7 @@ function n($value)
*/
function dt($format, $timestamp)
{
- $t = new Translator;
- return $t->datetime($format, $timestamp);
+ return Translator::getInstance()->datetime($format, $timestamp);
}
/**
@@ -200,6 +48,7 @@ function dt($format, $timestamp)
* @todo Improve this function
* @return mixed
*/
-function p($value, $t1, $t2) {
+function p($value, $t1, $t2)
+{
return $value > 1 ? $t2 : $t1;
}
diff --git a/app/helpers.php b/app/helpers.php
deleted file mode 100644
index cd6d630e..00000000
--- a/app/helpers.php
+++ /dev/null
@@ -1,662 +0,0 @@
-<?php
-
-namespace Helper;
-
-/**
- * Template helpers
- *
- */
-
-use Core\Security;
-use Core\Template;
-use Core\Tool;
-use Parsedown\Parsedown;
-
-/**
- * Append a CSRF token to a query string
- *
- * @return string
- */
-function param_csrf()
-{
- return '&amp;csrf_token='.Security::getCSRFToken();
-}
-
-/**
- * Add a Javascript asset
- *
- * @param string $filename Filename
- * @return string
- */
-function js($filename)
-{
- return '<script type="text/javascript" src="'.$filename.'?'.filemtime($filename).'"></script>';
-}
-
-/**
- * Add a stylesheet asset
- *
- * @param string $filename Filename
- * @return string
- */
-function css($filename)
-{
- return '<link rel="stylesheet" href="'.$filename.'?'.filemtime($filename).'" media="screen">';
-}
-
-/**
- * Load a template
- *
- * @param string $name Template name
- * @param array $args Template parameters
- * @return string
- */
-function template($name, array $args = array())
-{
- $tpl = new Template;
- return $tpl->load($name, $args);
-}
-
-/**
- * Check if the given user_id is the connected user
- *
- * @param integer $user_id User id
- * @return boolean
- */
-function is_current_user($user_id)
-{
- return $_SESSION['user']['id'] == $user_id;
-}
-
-/**
- * Check if the current user is administrator
- *
- * @return boolean
- */
-function is_admin()
-{
- return $_SESSION['user']['is_admin'] == 1;
-}
-
-/**
- * Return the username
- *
- * @param array $user User properties (optional)
- * @return string
- */
-function get_username(array $user = array())
-{
- return ! empty($user) ? ($user['name'] ?: $user['username'])
- : ($_SESSION['user']['name'] ?: $_SESSION['user']['username']);
-}
-
-/**
- * Get the current user id
- *
- * @return integer
- */
-function get_user_id()
-{
- return $_SESSION['user']['id'];
-}
-
-/**
- * Markdown transformation
- *
- * @param string $text Markdown content
- * @param array $link Link parameters for replacement
- * @return string
- */
-function markdown($text, array $link = array('controller' => 'task', 'action' => 'show', 'params' => array()))
-{
- $html = Parsedown::instance()
- ->setMarkupEscaped(true) # escapes markup (HTML)
- ->text($text);
-
- // Replace task #123 by a link to the task
- $html = preg_replace_callback('!#(\d+)!i', function($matches) use ($link) {
- return a(
- $matches[0],
- $link['controller'],
- $link['action'],
- $link['params'] + array('task_id' => $matches[1])
- );
- }, $html);
-
- return $html;
-}
-
-/**
- * Get the current URL without the querystring
- *
- * @return string
- */
-function get_current_base_url()
-{
- $url = Tool::isHTTPS() ? 'https://' : 'http://';
- $url .= $_SERVER['SERVER_NAME'];
- $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT'];
- $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/';
-
- return $url;
-}
-
-/**
- * HTML escaping
- *
- * @param string $value Value to escape
- * @return string
- */
-function escape($value)
-{
- return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
-}
-
-/**
- * Dispplay the flash session message
- *
- * @param string $html HTML wrapper
- * @return string
- */
-function flash($html)
-{
- $data = '';
-
- if (isset($_SESSION['flash_message'])) {
- $data = sprintf($html, escape($_SESSION['flash_message']));
- unset($_SESSION['flash_message']);
- }
-
- return $data;
-}
-
-/**
- * Display the flash session error message
- *
- * @param string $html HTML wrapper
- * @return string
- */
-function flash_error($html)
-{
- $data = '';
-
- if (isset($_SESSION['flash_error_message'])) {
- $data = sprintf($html, escape($_SESSION['flash_error_message']));
- unset($_SESSION['flash_error_message']);
- }
-
- return $data;
-}
-
-/**
- * Format a file size
- *
- * @param integer $size Size in bytes
- * @param integer $precision Precision
- * @return string
- */
-function format_bytes($size, $precision = 2)
-{
- $base = log($size) / log(1024);
- $suffixes = array('', 'k', 'M', 'G', 'T');
-
- return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)];
-}
-
-/**
- * Truncate a long text
- *
- * @param string $value Text
- * @param integer $max_length Max Length
- * @param string $end Text end
- * @return string
- */
-function summary($value, $max_length = 85, $end = '[...]')
-{
- $length = strlen($value);
-
- if ($length > $max_length) {
- return substr($value, 0, $max_length).' '.$end;
- }
-
- return $value;
-}
-
-/**
- * Return true if needle is contained in the haystack
- *
- * @param string $haystack Haystack
- * @param string $needle Needle
- * @return boolean
- */
-function contains($haystack, $needle)
-{
- return strpos($haystack, $needle) !== false;
-}
-
-/**
- * Return a value from a dictionary
- *
- * @param mixed $id Key
- * @param array $listing Dictionary
- * @param string $default_value Value displayed when the key doesn't exists
- * @return string
- */
-function in_list($id, array $listing, $default_value = '?')
-{
- if (isset($listing[$id])) {
- return escape($listing[$id]);
- }
-
- return $default_value;
-}
-
-/**
- * Display the form error class
- *
- * @param array $errors Error list
- * @param string $name Field name
- * @return string
- */
-function error_class(array $errors, $name)
-{
- return ! isset($errors[$name]) ? '' : ' form-error';
-}
-
-/**
- * Display a list of form errors
- *
- * @param array $errors List of errors
- * @param string $name Field name
- * @return string
- */
-function error_list(array $errors, $name)
-{
- $html = '';
-
- if (isset($errors[$name])) {
-
- $html .= '<ul class="form-errors">';
-
- foreach ($errors[$name] as $error) {
- $html .= '<li>'.escape($error).'</li>';
- }
-
- $html .= '</ul>';
- }
-
- return $html;
-}
-
-/**
- * Get an escaped form value
- *
- * @param mixed $values Values
- * @param string $name Field name
- * @return string
- */
-function form_value($values, $name)
-{
- if (isset($values->$name)) {
- return 'value="'.escape($values->$name).'"';
- }
-
- return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : '';
-}
-
-/**
- * Hidden CSRF token field
- *
- * @return string
- */
-function form_csrf()
-{
- return '<input type="hidden" name="csrf_token" value="'.Security::getCSRFToken().'"/>';
-}
-
-/**
- * Display a hidden form field
- *
- * @param string $name Field name
- * @param array $values Form values
- * @return string
- */
-function form_hidden($name, array $values = array())
-{
- return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).'/>';
-}
-
-/**
- * Display a select field
- *
- * @param string $name Field name
- * @param array $options Options
- * @param array $values Form values
- * @param array $errors Form errors
- * @param string $class CSS class
- * @return string
- */
-function form_select($name, array $options, array $values = array(), array $errors = array(), $class = '')
-{
- $html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'">';
-
- foreach ($options as $id => $value) {
-
- $html .= '<option value="'.escape($id).'"';
-
- if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"';
- if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"';
-
- $html .= '>'.escape($value).'</option>';
- }
-
- $html .= '</select>';
- $html .= error_list($errors, $name);
-
- return $html;
-}
-
-/**
- * Display a radio field group
- *
- * @param string $name Field name
- * @param array $options Options
- * @param array $values Form values
- * @return string
- */
-function form_radios($name, array $options, array $values = array())
-{
- $html = '';
-
- foreach ($options as $value => $label) {
- $html .= form_radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value);
- }
-
- return $html;
-}
-
-/**
- * Display a radio field
- *
- * @param string $name Field name
- * @param string $label Form label
- * @param string $value Form value
- * @param boolean $selected Field selected or not
- * @param string $class CSS class
- * @return string
- */
-function form_radio($name, $label, $value, $selected = false, $class = '')
-{
- return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($selected ? 'selected="selected"' : '').'>'.escape($label).'</label>';
-}
-
-/**
- * Display a checkbox field
- *
- * @param string $name Field name
- * @param string $label Form label
- * @param string $value Form value
- * @param boolean $checked Field selected or not
- * @param string $class CSS class
- * @return string
- */
-function form_checkbox($name, $label, $value, $checked = false, $class = '')
-{
- return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($checked ? 'checked="checked"' : '').'>&nbsp;'.escape($label).'</label>';
-}
-
-/**
- * Display a form label
- *
- * @param string $name Field name
- * @param string $label Form label
- * @param array $attributes HTML attributes
- * @return string
- */
-function form_label($label, $name, array $attributes = array())
-{
- return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.escape($label).'</label>';
-}
-
-/**
- * Display a textarea
- *
- * @param string $name Field name
- * @param array $values Form values
- * @param array $errors Form errors
- * @param array $attributes HTML attributes
- * @param string $class CSS class
- * @return string
- */
-function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
-{
- $class .= error_class($errors, $name);
-
- $html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" ';
- $html .= implode(' ', $attributes).'>';
- $html .= isset($values->$name) ? escape($values->$name) : isset($values[$name]) ? $values[$name] : '';
- $html .= '</textarea>';
- if (in_array('required', $attributes)) $html .= '<span class="form-required">*</span>';
- $html .= error_list($errors, $name);
-
- return $html;
-}
-
-/**
- * Display a input field
- *
- * @param string $type HMTL input tag type
- * @param string $name Field name
- * @param array $values Form values
- * @param array $errors Form errors
- * @param array $attributes HTML attributes
- * @param string $class CSS class
- * @return string
- */
-function form_input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
-{
- $class .= error_class($errors, $name);
-
- $html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" ';
- $html .= implode(' ', $attributes).'/>';
- if (in_array('required', $attributes)) $html .= '<span class="form-required">*</span>';
- $html .= error_list($errors, $name);
-
- return $html;
-}
-
-/**
- * Display a text field
- *
- * @param string $name Field name
- * @param array $values Form values
- * @param array $errors Form errors
- * @param array $attributes HTML attributes
- * @param string $class CSS class
- * @return string
- */
-function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
-{
- return form_input('text', $name, $values, $errors, $attributes, $class);
-}
-
-/**
- * Display a password field
- *
- * @param string $name Field name
- * @param array $values Form values
- * @param array $errors Form errors
- * @param array $attributes HTML attributes
- * @param string $class CSS class
- * @return string
- */
-function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
-{
- return form_input('password', $name, $values, $errors, $attributes, $class);
-}
-
-/**
- * Display an email field
- *
- * @param string $name Field name
- * @param array $values Form values
- * @param array $errors Form errors
- * @param array $attributes HTML attributes
- * @param string $class CSS class
- * @return string
- */
-function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
-{
- return form_input('email', $name, $values, $errors, $attributes, $class);
-}
-
-/**
- * Display a date field
- *
- * @param string $name Field name
- * @param array $values Form values
- * @param array $errors Form errors
- * @param array $attributes HTML attributes
- * @param string $class CSS class
- * @return string
- */
-function form_date($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
-{
- return form_input('date', $name, $values, $errors, $attributes, $class);
-}
-
-/**
- * Display a number field
- *
- * @param string $name Field name
- * @param array $values Form values
- * @param array $errors Form errors
- * @param array $attributes HTML attributes
- * @param string $class CSS class
- * @return string
- */
-function form_number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
-{
- return form_input('number', $name, $values, $errors, $attributes, $class);
-}
-
-/**
- * Display a numeric field (allow decimal number)
- *
- * @param string $name Field name
- * @param array $values Form values
- * @param array $errors Form errors
- * @param array $attributes HTML attributes
- * @param string $class CSS class
- * @return string
- */
-function form_numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
-{
- return form_input('text', $name, $values, $errors, $attributes, $class.' form-numeric');
-}
-
-/**
- * Link
- *
- * a('link', 'task', 'show', array('task_id' => $task_id))
- *
- * @param string $label Link label
- * @param string $controller Controller name
- * @param string $action Action name
- * @param array $params Url parameters
- * @param boolean $csrf Add a CSRF token
- * @param string $class CSS class attribute
- * @return string
- */
-function a($label, $controller, $action, array $params = array(), $csrf = false, $class = '')
-{
- return '<a href="'.u($controller, $action, $params, $csrf).'" class="'.$class.'"/>'.$label.'</a>';
-}
-
-/**
- * URL query string
- *
- * u('task', 'show', array('task_id' => $task_id))
- *
- * @param string $controller Controller name
- * @param string $action Action name
- * @param array $params Url parameters
- * @param boolean $csrf Add a CSRF token
- * @return string
- */
-function u($controller, $action, array $params = array(), $csrf = false)
-{
- $html = '?controller='.$controller.'&amp;action='.$action;
-
- if ($csrf) {
- $params['csrf_token'] = Security::getCSRFToken();
- }
-
- foreach ($params as $key => $value) {
- $html .= '&amp;'.$key.'='.$value;
- }
-
- return $html;
-}
-
-/**
- * Pagination links
- *
- * @param array $pagination Pagination information
- * @return string
- */
-function paginate(array $pagination)
-{
- extract($pagination);
-
- $html = '<div id="pagination">';
- $html .= '<span id="pagination-previous">';
-
- if ($pagination['offset'] > 0) {
- $offset = $pagination['offset'] - $limit;
- $html .= a('&larr; '.t('Previous'), $controller, $action, $params + compact('offset', 'order', 'direction'));
- }
- else {
- $html .= '&larr; '.t('Previous');
- }
-
- $html .= '</span>';
- $html .= '<span id="pagination-next">';
-
- if (($total - $pagination['offset']) > $limit) {
- $offset = $pagination['offset'] + $limit;
- $html .= a(t('Next').' &rarr;', $controller, $action, $params + compact('offset', 'order', 'direction'));
- }
- else {
- $html .= t('Next').' &rarr;';
- }
-
- $html .= '</span>';
- $html .= '</div>';
-
- return $html;
-}
-
-/**
- * Column sorting (work with pagination)
- *
- * @param string $label Column title
- * @param string $column SQL column name
- * @param array $pagination Pagination information
- * @return string
- */
-function order($label, $column, array $pagination)
-{
- extract($pagination);
-
- $prefix = '';
-
- if ($order === $column) {
- $prefix = $direction === 'DESC' ? '&#9660; ' : '&#9650; ';
- $direction = $direction === 'DESC' ? 'ASC' : 'DESC';
- }
-
- $order = $column;
-
- return $prefix.a($label, $controller, $action, $params + compact('offset', 'order', 'direction'));
-}