From 6cc879dffe7796ac72863e048d26d97b21230ec3 Mon Sep 17 00:00:00 2001 From: mbugeia Date: Sat, 23 Jul 2016 14:11:39 +0200 Subject: [PATCH] Update kanboard to v1.0.31 --- README.markdown | 4 +- conf/config.php | 31 +- sources/.htaccess | 26 + sources/ChangeLog | 132 + sources/app/Action/CommentCreation.php | 4 +- .../Action/CommentCreationMoveTaskColumn.php | 8 +- .../app/Action/TaskAssignCategoryColor.php | 6 +- .../app/Action/TaskAssignCategoryLabel.php | 2 +- sources/app/Action/TaskAssignCategoryLink.php | 8 +- .../app/Action/TaskAssignColorCategory.php | 6 +- sources/app/Action/TaskAssignColorColumn.php | 8 +- sources/app/Action/TaskAssignColorLink.php | 6 +- .../app/Action/TaskAssignColorPriority.php | 95 + sources/app/Action/TaskAssignColorUser.php | 8 +- sources/app/Action/TaskAssignCurrentUser.php | 6 +- .../Action/TaskAssignCurrentUserColumn.php | 6 +- sources/app/Action/TaskAssignSpecificUser.php | 8 +- sources/app/Action/TaskAssignUser.php | 4 +- sources/app/Action/TaskClose.php | 2 +- sources/app/Action/TaskCloseColumn.php | 6 +- sources/app/Action/TaskCloseNoActivity.php | 6 +- sources/app/Action/TaskCreation.php | 2 +- .../Action/TaskDuplicateAnotherProject.php | 11 +- sources/app/Action/TaskEmail.php | 12 +- sources/app/Action/TaskEmailNoActivity.php | 10 +- sources/app/Action/TaskMoveAnotherProject.php | 8 +- sources/app/Action/TaskMoveColumnAssigned.php | 10 +- .../Action/TaskMoveColumnCategoryChange.php | 8 +- .../app/Action/TaskMoveColumnUnAssigned.php | 10 +- sources/app/Action/TaskOpen.php | 2 +- sources/app/Action/TaskUpdateStartDate.php | 6 +- .../Analytic/AverageLeadCycleTimeAnalytic.php | 4 +- .../AverageTimeSpentColumnAnalytic.php | 8 +- .../EstimatedTimeComparisonAnalytic.php | 6 +- .../app/Analytic/TaskDistributionAnalytic.php | 4 +- .../app/Analytic/UserDistributionAnalytic.php | 4 +- .../Api/Authorization/ActionAuthorization.php | 19 + .../Authorization/CategoryAuthorization.php | 19 + .../Api/Authorization/ColumnAuthorization.php | 19 + .../Authorization/CommentAuthorization.php | 19 + .../Authorization/ProcedureAuthorization.php | 32 + .../Authorization/ProjectAuthorization.php | 35 + .../Authorization/SubtaskAuthorization.php | 19 + .../Api/Authorization/TaskAuthorization.php | 19 + .../Authorization/TaskFileAuthorization.php | 19 + .../Authorization/TaskLinkAuthorization.php | 19 + .../Api/Authorization/UserAuthorization.php | 22 + sources/app/Api/Base.php | 117 - sources/app/Api/Board.php | 18 - sources/app/Api/Category.php | 49 - sources/app/Api/Column.php | 42 - sources/app/Api/Comment.php | 52 - sources/app/Api/File.php | 90 - sources/app/Api/GroupMember.php | 32 - sources/app/Api/Me.php | 72 - .../AuthenticationMiddleware.php} | 43 +- .../ActionProcedure.php} | 18 +- .../{App.php => Procedure/AppProcedure.php} | 14 +- sources/app/Api/Procedure/BaseProcedure.php | 85 + sources/app/Api/Procedure/BoardProcedure.php | 25 + .../app/Api/Procedure/CategoryProcedure.php | 59 + sources/app/Api/Procedure/ColumnProcedure.php | 51 + .../app/Api/Procedure/CommentProcedure.php | 62 + .../Api/Procedure/GroupMemberProcedure.php | 37 + .../GroupProcedure.php} | 16 +- .../{Link.php => Procedure/LinkProcedure.php} | 20 +- sources/app/Api/Procedure/MeProcedure.php | 72 + .../Api/Procedure/ProjectFileProcedure.php | 68 + .../Procedure/ProjectPermissionProcedure.php | 69 + .../app/Api/Procedure/ProjectProcedure.php | 113 + .../SubtaskProcedure.php} | 26 +- .../SubtaskTimeTrackingProcedure.php | 39 + .../app/Api/Procedure/SwimlaneProcedure.php | 91 + .../Procedure/TaskExternalLinkProcedure.php | 106 + .../app/Api/Procedure/TaskFileProcedure.php | 70 + .../TaskLinkProcedure.php} | 24 +- .../{Task.php => Procedure/TaskProcedure.php} | 89 +- .../{User.php => Procedure/UserProcedure.php} | 36 +- sources/app/Api/Project.php | 87 - sources/app/Api/ProjectPermission.php | 72 - sources/app/Api/Swimlane.php | 78 - sources/app/Auth/DatabaseAuth.php | 6 +- sources/app/Auth/LdapAuth.php | 2 +- sources/app/Auth/RememberMeAuth.php | 6 +- sources/app/Auth/ReverseProxyAuth.php | 3 +- sources/app/Console/Base.php | 61 - sources/app/Console/BaseCommand.php | 67 + .../{Cronjob.php => CronjobCommand.php} | 2 +- ...arator.php => LocaleComparatorCommand.php} | 2 +- .../{LocaleSync.php => LocaleSyncCommand.php} | 2 +- sources/app/Console/PluginInstallCommand.php | 36 + .../app/Console/PluginUninstallCommand.php | 36 + sources/app/Console/PluginUpgradeCommand.php | 55 + ... ProjectDailyColumnStatsExportCommand.php} | 4 +- ...> ProjectDailyStatsCalculationCommand.php} | 10 +- sources/app/Console/ResetPasswordCommand.php | 79 + sources/app/Console/ResetTwoFactorCommand.php | 38 + ...askExport.php => SubtaskExportCommand.php} | 2 +- .../{TaskExport.php => TaskExportCommand.php} | 2 +- .../app/Console/TaskOverdueNotification.php | 116 - .../TaskOverdueNotificationCommand.php | 191 + ...TaskTrigger.php => TaskTriggerCommand.php} | 10 +- ...Export.php => TransitionExportCommand.php} | 2 +- sources/app/Console/WorkerCommand.php | 28 + .../{Action.php => ActionController.php} | 28 +- ...ation.php => ActionCreationController.php} | 27 +- .../{Activity.php => ActivityController.php} | 12 +- .../{Analytic.php => AnalyticController.php} | 27 +- sources/app/Controller/AppController.php | 46 + .../{Auth.php => AuthController.php} | 36 +- ...vatarFile.php => AvatarFileController.php} | 20 +- sources/app/Controller/Base.php | 290 -- sources/app/Controller/BaseController.php | 158 + sources/app/Controller/Board.php | 186 - .../app/Controller/BoardAjaxController.php | 140 + sources/app/Controller/BoardPopover.php | 135 - .../app/Controller/BoardPopoverController.php | 47 + ...Tooltip.php => BoardTooltipController.php} | 22 +- .../app/Controller/BoardViewController.php | 69 + .../{Calendar.php => CalendarController.php} | 53 +- .../{Captcha.php => CaptchaController.php} | 6 +- .../{Category.php => CategoryController.php} | 47 +- .../{Column.php => ColumnController.php} | 41 +- .../{Comment.php => CommentController.php} | 43 +- .../{Config.php => ConfigController.php} | 143 +- .../{Currency.php => CurrencyController.php} | 25 +- ...mfilter.php => CustomFilterController.php} | 45 +- .../{App.php => DashboardController.php} | 58 +- sources/app/Controller/Doc.php | 92 - .../Controller/DocumentationController.php | 136 + .../{Export.php => ExportController.php} | 47 +- .../{Feed.php => FeedController.php} | 19 +- ...ileViewer.php => FileViewerController.php} | 56 +- sources/app/Controller/Gantt.php | 153 - sources/app/Controller/Group.php | 250 - .../app/Controller/GroupAjaxController.php | 26 + .../Controller/GroupCreationController.php | 49 + sources/app/Controller/GroupHelper.php | 24 - .../app/Controller/GroupListController.php | 173 + .../GroupModificationController.php | 53 + .../app/Controller/ICalendarController.php | 101 + sources/app/Controller/Ical.php | 115 - .../{Link.php => LinkController.php} | 38 +- .../{Oauth.php => OAuthController.php} | 14 +- ...dReset.php => PasswordResetController.php} | 46 +- sources/app/Controller/PluginController.php | 126 + sources/app/Controller/Project.php | 243 - ...=> ProjectActionDuplicationController.php} | 14 +- ...tion.php => ProjectCreationController.php} | 20 +- ...jectEdit.php => ProjectEditController.php} | 26 +- ...jectFile.php => ProjectFileController.php} | 16 +- .../app/Controller/ProjectGanttController.php | 57 + .../app/Controller/ProjectListController.php | 41 + ...view.php => ProjectOverviewController.php} | 14 +- ...on.php => ProjectPermissionController.php} | 45 +- .../Controller/ProjectStatusController.php | 102 + .../app/Controller/ProjectTagController.php | 134 + ....php => ProjectUserOverviewController.php} | 32 +- .../app/Controller/ProjectViewController.php | 162 + sources/app/Controller/Search.php | 49 - sources/app/Controller/SearchController.php | 67 + .../{Subtask.php => SubtaskController.php} | 49 +- .../Controller/SubtaskConverterController.php | 39 + ...n.php => SubtaskRestrictionController.php} | 20 +- ...Status.php => SubtaskStatusController.php} | 15 +- .../{Swimlane.php => SwimlaneController.php} | 86 +- sources/app/Controller/TagController.php | 121 + sources/app/Controller/Task.php | 174 - sources/app/Controller/TaskAjaxController.php | 49 + sources/app/Controller/TaskBulkController.php | 89 + ...reation.php => TaskCreationController.php} | 32 +- ...tion.php => TaskDuplicationController.php} | 36 +- ...ink.php => TaskExternalLinkController.php} | 45 +- .../{TaskFile.php => TaskFileController.php} | 22 +- .../app/Controller/TaskGanttController.php | 62 + .../TaskGanttCreationController.php | 70 + sources/app/Controller/TaskHelper.php | 41 - sources/app/Controller/TaskImport.php | 72 - .../app/Controller/TaskImportController.php | 74 + ...ink.php => TaskInternalLinkController.php} | 41 +- .../{Listing.php => TaskListController.php} | 20 +- .../Controller/TaskModificationController.php | 78 + .../app/Controller/TaskPopoverController.php | 26 + ...rence.php => TaskRecurrenceController.php} | 22 +- ...askstatus.php => TaskStatusController.php} | 10 +- .../Controller/TaskSuppressionController.php | 53 + sources/app/Controller/TaskViewController.php | 147 + sources/app/Controller/Taskmodification.php | 122 - ...{Twofactor.php => TwoFactorController.php} | 30 +- sources/app/Controller/User.php | 408 -- sources/app/Controller/UserAjaxController.php | 52 + .../app/Controller/UserCreationController.php | 83 + .../Controller/UserCredentialController.php | 109 + sources/app/Controller/UserHelper.php | 37 - ...serImport.php => UserImportController.php} | 53 +- sources/app/Controller/UserListController.php | 32 + .../Controller/UserModificationController.php | 69 + ...serStatus.php => UserStatusController.php} | 16 +- sources/app/Controller/UserViewController.php | 217 + sources/app/Controller/WebNotification.php | 50 - .../Controller/WebNotificationController.php | 79 + sources/app/Controller/Webhook.php | 42 - sources/app/Core/Action/ActionManager.php | 6 +- sources/app/Core/Base.php | 289 +- .../Controller/AccessForbiddenException.php | 14 + sources/app/Core/Controller/BaseException.php | 52 + .../app/Core/Controller/BaseMiddleware.php | 58 + .../Core/Controller/PageNotFoundException.php | 14 + sources/app/Core/Controller/Runner.php | 105 + sources/app/Core/DateParser.php | 78 +- sources/app/Core/Event/EventManager.php | 22 +- .../Core/ExternalLink/ExternalLinkManager.php | 26 +- sources/app/Core/Filter/CriteriaInterface.php | 40 + sources/app/Core/Filter/FilterInterface.php | 56 + .../app/Core/Filter/FormatterInterface.php | 31 + sources/app/Core/Filter/Lexer.php | 155 + sources/app/Core/Filter/LexerBuilder.php | 151 + sources/app/Core/Filter/OrCriteria.php | 68 + sources/app/Core/Filter/QueryBuilder.php | 103 + sources/app/Core/Helper.php | 6 +- sources/app/Core/Http/Client.php | 54 +- sources/app/Core/Http/Response.php | 451 +- sources/app/Core/Http/Route.php | 4 +- sources/app/Core/Http/Router.php | 62 +- sources/app/Core/Ldap/Group.php | 7 +- sources/app/Core/Ldap/Query.php | 4 + sources/app/Core/Ldap/User.php | 160 +- sources/app/Core/Lexer.php | 161 - sources/app/Core/Mail/Client.php | 47 +- sources/app/Core/Mail/Transport/Mail.php | 2 +- sources/app/Core/Markdown.php | 85 +- .../Notification/NotificationInterface.php | 4 +- sources/app/Core/Plugin/Base.php | 16 +- sources/app/Core/Plugin/Directory.php | 56 + sources/app/Core/Plugin/Hook.php | 4 +- sources/app/Core/Plugin/Installer.php | 162 + sources/app/Core/Plugin/Loader.php | 155 +- .../Core/Plugin/PluginInstallerException.php | 15 + sources/app/Core/Plugin/SchemaHandler.php | 122 + sources/app/Core/Queue/JobHandler.php | 70 + sources/app/Core/Queue/QueueManager.php | 71 + sources/app/Core/Session/SessionStorage.php | 1 + sources/app/Core/Template.php | 2 +- sources/app/Core/Tool.php | 20 - sources/app/Core/User/GroupSync.php | 50 +- sources/app/Core/User/UserProfile.php | 10 +- sources/app/Core/User/UserProperty.php | 6 +- sources/app/Core/User/UserSession.php | 2 +- sources/app/Core/User/UserSync.php | 10 +- sources/app/Event/UserProfileSyncEvent.php | 64 + sources/app/Export/SubtaskExport.php | 24 +- sources/app/Export/TaskExport.php | 8 +- sources/app/Export/TransitionExport.php | 5 +- sources/app/Filter/BaseDateFilter.php | 103 + sources/app/Filter/BaseFilter.php | 74 + .../ProjectActivityCreationDateFilter.php | 38 + .../Filter/ProjectActivityCreatorFilter.php | 65 + .../Filter/ProjectActivityProjectIdFilter.php | 38 + .../ProjectActivityProjectIdsFilter.php | 43 + .../ProjectActivityProjectNameFilter.php | 38 + .../Filter/ProjectActivityTaskIdFilter.php | 38 + .../ProjectActivityTaskStatusFilter.php | 43 + .../Filter/ProjectActivityTaskTitleFilter.php | 25 + .../Filter/ProjectGroupRoleProjectFilter.php | 38 + .../Filter/ProjectGroupRoleUsernameFilter.php | 44 + sources/app/Filter/ProjectIdsFilter.php | 43 + sources/app/Filter/ProjectStatusFilter.php | 45 + sources/app/Filter/ProjectTypeFilter.php | 45 + .../Filter/ProjectUserRoleProjectFilter.php | 38 + .../Filter/ProjectUserRoleUsernameFilter.php | 41 + sources/app/Filter/TaskAssigneeFilter.php | 75 + sources/app/Filter/TaskCategoryFilter.php | 46 + sources/app/Filter/TaskColorFilter.php | 60 + sources/app/Filter/TaskColumnFilter.php | 44 + sources/app/Filter/TaskCommentFilter.php | 41 + .../app/Filter/TaskCompletionDateFilter.php | 38 + sources/app/Filter/TaskCreationDateFilter.php | 38 + sources/app/Filter/TaskCreatorFilter.php | 74 + sources/app/Filter/TaskDescriptionFilter.php | 38 + sources/app/Filter/TaskDueDateFilter.php | 41 + sources/app/Filter/TaskDueDateRangeFilter.php | 39 + sources/app/Filter/TaskIdExclusionFilter.php | 38 + sources/app/Filter/TaskIdFilter.php | 38 + sources/app/Filter/TaskLinkFilter.php | 85 + .../app/Filter/TaskModificationDateFilter.php | 38 + sources/app/Filter/TaskProjectFilter.php | 44 + sources/app/Filter/TaskProjectsFilter.php | 43 + sources/app/Filter/TaskReferenceFilter.php | 38 + sources/app/Filter/TaskStartDateFilter.php | 38 + sources/app/Filter/TaskStatusFilter.php | 43 + .../app/Filter/TaskSubtaskAssigneeFilter.php | 140 + sources/app/Filter/TaskSwimlaneFilter.php | 50 + sources/app/Filter/TaskTagFilter.php | 74 + sources/app/Filter/TaskTitleFilter.php | 46 + sources/app/Filter/UserNameFilter.php | 35 + sources/app/Formatter/BaseFormatter.php | 50 + ...vent.php => BaseTaskCalendarFormatter.php} | 37 +- .../app/Formatter/BoardColumnFormatter.php | 94 + sources/app/Formatter/BoardFormatter.php | 66 + .../app/Formatter/BoardSwimlaneFormatter.php | 120 + sources/app/Formatter/BoardTaskFormatter.php | 96 + sources/app/Formatter/FormatterInterface.php | 14 - .../Formatter/GroupAutoCompleteFormatter.php | 28 +- .../ProjectActivityEventFormatter.php | 61 + .../app/Formatter/ProjectGanttFormatter.php | 49 +- .../SubtaskTimeTrackingCalendarFormatter.php | 38 + .../Formatter/TaskAutoCompleteFormatter.php | 33 + .../app/Formatter/TaskCalendarFormatter.php | 74 + .../TaskFilterAutoCompleteFormatter.php | 33 - .../Formatter/TaskFilterCalendarFormatter.php | 52 - ...ttFormatter.php => TaskGanttFormatter.php} | 22 +- ...darFormatter.php => TaskICalFormatter.php} | 36 +- ...tter.php => UserAutoCompleteFormatter.php} | 10 +- .../Group/DatabaseBackendGroupProvider.php | 2 +- sources/app/Helper/AppHelper.php | 39 +- sources/app/Helper/AssetHelper.php | 6 +- sources/app/Helper/CalendarHelper.php | 112 + sources/app/Helper/DateHelper.php | 6 +- sources/app/Helper/ICalHelper.php | 38 + sources/app/Helper/LayoutHelper.php | 24 +- sources/app/Helper/MailHelper.php | 82 + sources/app/Helper/ProjectActivityHelper.php | 105 + sources/app/Helper/ProjectHeaderHelper.php | 6 +- sources/app/Helper/SubtaskHelper.php | 6 +- sources/app/Helper/TaskHelper.php | 88 +- sources/app/Helper/TextHelper.php | 20 +- sources/app/Helper/UrlHelper.php | 4 +- sources/app/Helper/UserHelper.php | 49 +- sources/app/Import/TaskImport.php | 14 +- sources/app/Import/UserImport.php | 6 +- sources/app/Job/BaseJob.php | 33 + sources/app/Job/EmailJob.php | 55 + sources/app/Job/HttpAsyncJob.php | 43 + sources/app/Job/NotificationJob.php | 85 + sources/app/Job/ProjectMetricJob.php | 40 + sources/app/Locale/bs_BA/translations.php | 143 +- sources/app/Locale/cs_CZ/translations.php | 225 +- sources/app/Locale/da_DK/translations.php | 91 +- sources/app/Locale/de_DE/translations.php | 189 +- sources/app/Locale/el_GR/translations.php | 91 +- sources/app/Locale/es_ES/translations.php | 945 ++-- sources/app/Locale/fi_FI/translations.php | 91 +- sources/app/Locale/fr_FR/translations.php | 94 +- sources/app/Locale/hu_HU/translations.php | 1257 ++--- sources/app/Locale/id_ID/translations.php | 91 +- sources/app/Locale/it_IT/translations.php | 145 +- sources/app/Locale/ja_JP/translations.php | 91 +- sources/app/Locale/ko_KR/translations.php | 965 ++-- sources/app/Locale/my_MY/translations.php | 91 +- sources/app/Locale/nb_NO/translations.php | 91 +- sources/app/Locale/nl_NL/translations.php | 91 +- sources/app/Locale/pl_PL/translations.php | 595 +-- sources/app/Locale/pt_BR/translations.php | 295 +- sources/app/Locale/pt_PT/translations.php | 129 +- sources/app/Locale/ru_RU/translations.php | 99 +- .../app/Locale/sr_Latn_RS/translations.php | 91 +- sources/app/Locale/sv_SE/translations.php | 91 +- sources/app/Locale/th_TH/translations.php | 91 +- sources/app/Locale/tr_TR/translations.php | 91 +- sources/app/Locale/zh_CN/translations.php | 91 +- .../ApplicationAuthorizationMiddleware.php | 27 + .../Middleware/AuthenticationMiddleware.php | 56 + .../app/Middleware/BootstrapMiddleware.php | 44 + .../PostAuthenticationMiddleware.php | 36 + .../ProjectAuthorizationMiddleware.php | 34 + .../app/Model/{Action.php => ActionModel.php} | 34 +- ...Parameter.php => ActionParameterModel.php} | 14 +- .../{AvatarFile.php => AvatarFileModel.php} | 54 +- sources/app/Model/Base.php | 58 - .../app/Model/{Board.php => BoardModel.php} | 76 +- .../Model/{Category.php => CategoryModel.php} | 24 +- .../app/Model/{Color.php => ColorModel.php} | 8 +- .../app/Model/{Column.php => ColumnModel.php} | 20 +- .../Model/{Comment.php => CommentModel.php} | 45 +- sources/app/Model/Config.php | 255 - sources/app/Model/ConfigModel.php | 89 + .../Model/{Currency.php => CurrencyModel.php} | 8 +- ...CustomFilter.php => CustomFilterModel.php} | 14 +- sources/app/Model/{File.php => FileModel.php} | 88 +- .../{GroupMember.php => GroupMemberModel.php} | 27 +- .../app/Model/{Group.php => GroupModel.php} | 10 +- sources/app/Model/LanguageModel.php | 179 + .../{LastLogin.php => LastLoginModel.php} | 8 +- sources/app/Model/{Link.php => LinkModel.php} | 5 +- .../Model/{Metadata.php => MetadataModel.php} | 30 +- ...Notification.php => NotificationModel.php} | 105 +- ...tionType.php => NotificationTypeModel.php} | 7 +- ...sswordReset.php => PasswordResetModel.php} | 8 +- sources/app/Model/ProjectActivity.php | 211 - sources/app/Model/ProjectActivityModel.php | 94 + ...s.php => ProjectDailyColumnStatsModel.php} | 20 +- ...lyStats.php => ProjectDailyStatsModel.php} | 6 +- ...cation.php => ProjectDuplicationModel.php} | 41 +- sources/app/Model/ProjectFile.php | 40 - sources/app/Model/ProjectFileModel.php | 74 + sources/app/Model/ProjectGroupRoleFilter.php | 89 - ...roupRole.php => ProjectGroupRoleModel.php} | 45 +- sources/app/Model/ProjectMetadata.php | 30 - sources/app/Model/ProjectMetadataModel.php | 54 + .../Model/{Project.php => ProjectModel.php} | 51 +- ...ation.php => ProjectNotificationModel.php} | 18 +- ...e.php => ProjectNotificationTypeModel.php} | 4 +- ...mission.php => ProjectPermissionModel.php} | 61 +- .../app/Model/ProjectTaskDuplicationModel.php | 35 + .../app/Model/ProjectTaskPriorityModel.php | 74 + sources/app/Model/ProjectUserRoleFilter.php | 88 - ...tUserRole.php => ProjectUserRoleModel.php} | 64 +- ...Session.php => RememberMeSessionModel.php} | 5 +- .../Model/{Setting.php => SettingModel.php} | 7 +- .../Model/{Subtask.php => SubtaskModel.php} | 94 +- ...cking.php => SubtaskTimeTrackingModel.php} | 157 +- .../Model/{Swimlane.php => SwimlaneModel.php} | 53 +- sources/app/Model/TagDuplicationModel.php | 87 + sources/app/Model/TagModel.php | 180 + sources/app/Model/Task.php | 223 - ...TaskAnalytic.php => TaskAnalyticModel.php} | 10 +- ...TaskCreation.php => TaskCreationModel.php} | 41 +- sources/app/Model/TaskDuplication.php | 269 -- sources/app/Model/TaskDuplicationModel.php | 145 + ...rnalLink.php => TaskExternalLinkModel.php} | 12 +- sources/app/Model/TaskFile.php | 54 - sources/app/Model/TaskFileModel.php | 104 + sources/app/Model/TaskFilter.php | 745 --- .../{TaskFinder.php => TaskFinderModel.php} | 238 +- .../Model/{TaskLink.php => TaskLinkModel.php} | 77 +- ...TaskMetadata.php => TaskMetadataModel.php} | 15 +- sources/app/Model/TaskModel.php | 146 + ...fication.php => TaskModificationModel.php} | 37 +- sources/app/Model/TaskPermission.php | 34 - ...TaskPosition.php => TaskPositionModel.php} | 27 +- .../app/Model/TaskProjectDuplicationModel.php | 60 + sources/app/Model/TaskProjectMoveModel.php | 68 + sources/app/Model/TaskRecurrenceModel.php | 147 + .../{TaskStatus.php => TaskStatusModel.php} | 31 +- sources/app/Model/TaskTagModel.php | 184 + sources/app/Model/TimezoneModel.php | 58 + .../{Transition.php => TransitionModel.php} | 32 +- sources/app/Model/UserFilter.php | 80 - .../{UserLocking.php => UserLockingModel.php} | 16 +- .../{UserMention.php => UserMentionModel.php} | 11 +- ...UserMetadata.php => UserMetadataModel.php} | 15 +- sources/app/Model/{User.php => UserModel.php} | 33 +- ...er.php => UserNotificationFilterModel.php} | 10 +- ...fication.php => UserNotificationModel.php} | 70 +- ...Type.php => UserNotificationTypeModel.php} | 4 +- ...on.php => UserUnreadNotificationModel.php} | 32 +- ...eam.php => ActivityStreamNotification.php} | 7 +- .../{Mail.php => MailNotification.php} | 47 +- .../{Web.php => WebNotification.php} | 7 +- .../{Webhook.php => WebhookNotification.php} | 9 +- sources/app/Schema/Mysql.php | 25 +- sources/app/Schema/Postgres.php | 24 +- sources/app/Schema/Sqlite.php | 24 +- .../app/ServiceProvider/ActionProvider.php | 4 +- sources/app/ServiceProvider/ApiProvider.php | 81 + .../AuthenticationProvider.php | 177 +- .../app/ServiceProvider/AvatarProvider.php | 2 +- sources/app/ServiceProvider/ClassProvider.php | 145 +- .../app/ServiceProvider/CommandProvider.php | 62 + .../app/ServiceProvider/DatabaseProvider.php | 25 +- .../EventDispatcherProvider.php | 11 + .../ServiceProvider/ExternalLinkProvider.php | 2 +- .../app/ServiceProvider/FilterProvider.php | 178 + sources/app/ServiceProvider/GroupProvider.php | 2 +- .../app/ServiceProvider/HelperProvider.php | 10 + .../app/ServiceProvider/LoggingProvider.php | 35 +- sources/app/ServiceProvider/MailProvider.php | 34 + .../ServiceProvider/NotificationProvider.php | 26 +- .../app/ServiceProvider/PluginProvider.php | 2 +- sources/app/ServiceProvider/QueueProvider.php | 27 + sources/app/ServiceProvider/RouteProvider.php | 222 +- .../app/ServiceProvider/SessionProvider.php | 2 +- sources/app/Subscriber/AuthSubscriber.php | 16 +- sources/app/Subscriber/BaseSubscriber.php | 1 - .../app/Subscriber/BootstrapSubscriber.php | 16 +- .../Subscriber/LdapUserPhotoSubscriber.php | 49 + .../app/Subscriber/NotificationSubscriber.php | 86 +- .../ProjectDailySummarySubscriber.php | 16 +- .../ProjectModificationDateSubscriber.php | 20 +- .../Subscriber/RecurringTaskSubscriber.php | 20 +- .../SubtaskTimeTrackingSubscriber.php | 20 +- .../app/Subscriber/TransitionSubscriber.php | 6 +- sources/app/Template/action/index.php | 8 +- sources/app/Template/action/remove.php | 6 +- .../app/Template/action_creation/create.php | 6 +- .../app/Template/action_creation/event.php | 6 +- .../app/Template/action_creation/params.php | 9 +- .../app/Template/activity/filter_dropdown.php | 14 + sources/app/Template/activity/project.php | 8 +- sources/app/Template/analytic/burndown.php | 2 +- sources/app/Template/analytic/cfd.php | 2 +- .../app/Template/analytic/compare_hours.php | 6 +- sources/app/Template/analytic/layout.php | 5 +- .../app/Template/analytic/lead_cycle_time.php | 2 +- sources/app/Template/analytic/sidebar.php | 32 +- sources/app/Template/app/calendar.php | 5 - sources/app/Template/app/sidebar.php | 27 - sources/app/Template/auth/index.php | 8 +- sources/app/Template/avatar_file/show.php | 19 +- .../app/Template/board/popover_assignee.php | 20 - .../app/Template/board/popover_category.php | 20 - sources/app/Template/board/table_column.php | 22 +- .../app/Template/board/table_container.php | 10 +- sources/app/Template/board/table_swimlane.php | 2 +- sources/app/Template/board/task_avatar.php | 4 +- sources/app/Template/board/task_footer.php | 44 +- sources/app/Template/board/task_menu.php | 18 - sources/app/Template/board/task_private.php | 16 +- sources/app/Template/board/task_public.php | 6 +- sources/app/Template/board/tooltip_files.php | 4 +- .../app/Template/board/tooltip_tasklinks.php | 4 +- sources/app/Template/board/view_private.php | 2 +- .../close_all_tasks_column.php} | 6 +- sources/app/Template/calendar/show.php | 8 +- sources/app/Template/category/edit.php | 6 +- sources/app/Template/category/index.php | 8 +- sources/app/Template/category/remove.php | 6 +- sources/app/Template/column/create.php | 4 +- sources/app/Template/column/edit.php | 6 +- sources/app/Template/column/index.php | 12 +- sources/app/Template/column/remove.php | 6 +- sources/app/Template/comment/create.php | 6 +- sources/app/Template/comment/edit.php | 4 +- sources/app/Template/comment/remove.php | 6 +- sources/app/Template/comment/show.php | 28 +- sources/app/Template/comments/create.php | 4 +- sources/app/Template/comments/show.php | 4 +- sources/app/Template/config/about.php | 10 +- sources/app/Template/config/api.php | 4 +- sources/app/Template/config/application.php | 2 +- sources/app/Template/config/board.php | 2 +- sources/app/Template/config/calendar.php | 4 +- sources/app/Template/config/email.php | 18 + sources/app/Template/config/integrations.php | 4 +- .../Template/config/keyboard_shortcuts.php | 3 +- sources/app/Template/config/layout.php | 3 +- sources/app/Template/config/plugins.php | 30 - sources/app/Template/config/project.php | 2 +- sources/app/Template/config/sidebar.php | 49 +- sources/app/Template/config/webhook.php | 12 +- sources/app/Template/currency/index.php | 4 +- sources/app/Template/custom_filter/add.php | 6 +- sources/app/Template/custom_filter/edit.php | 8 +- sources/app/Template/custom_filter/index.php | 6 +- sources/app/Template/custom_filter/remove.php | 4 +- .../Template/{app => dashboard}/activity.php | 0 sources/app/Template/dashboard/calendar.php | 5 + .../Template/{app => dashboard}/layout.php | 12 +- .../{app => dashboard}/notifications.php | 20 +- .../Template/{app => dashboard}/projects.php | 23 +- .../{app/overview.php => dashboard/show.php} | 8 +- sources/app/Template/dashboard/sidebar.php | 27 + .../Template/{app => dashboard}/subtasks.php | 8 +- .../app/Template/{app => dashboard}/tasks.php | 20 +- sources/app/Template/doc/show.php | 4 +- sources/app/Template/event/comment_create.php | 2 +- sources/app/Template/event/comment_update.php | 2 +- sources/app/Template/event/subtask_create.php | 4 +- sources/app/Template/event/subtask_update.php | 2 +- .../Template/event/task_assignee_change.php | 6 +- sources/app/Template/event/task_close.php | 2 +- sources/app/Template/event/task_create.php | 2 +- .../app/Template/event/task_file_create.php | 2 +- .../app/Template/event/task_move_column.php | 2 +- .../app/Template/event/task_move_position.php | 2 +- .../app/Template/event/task_move_swimlane.php | 4 +- sources/app/Template/event/task_open.php | 2 +- sources/app/Template/event/task_update.php | 2 +- sources/app/Template/export/sidebar.php | 18 +- sources/app/Template/feed/project.php | 8 +- sources/app/Template/feed/user.php | 8 +- sources/app/Template/file_viewer/show.php | 4 +- sources/app/Template/gantt/task_creation.php | 35 - sources/app/Template/group/associate.php | 41 +- sources/app/Template/group/create.php | 19 - sources/app/Template/group/dissociate.php | 25 +- sources/app/Template/group/edit.php | 22 - sources/app/Template/group/index.php | 14 +- sources/app/Template/group/remove.php | 25 +- sources/app/Template/group/users.php | 11 +- sources/app/Template/group_creation/show.php | 15 + .../app/Template/group_modification/show.php | 18 + sources/app/Template/header.php | 47 +- sources/app/Template/layout.php | 38 +- sources/app/Template/link/create.php | 4 +- sources/app/Template/link/edit.php | 4 +- sources/app/Template/link/index.php | 6 +- sources/app/Template/link/remove.php | 6 +- sources/app/Template/notification/footer.php | 4 +- .../Template/notification/task_overdue.php | 43 +- .../app/Template/password_reset/change.php | 4 +- .../app/Template/password_reset/create.php | 6 +- sources/app/Template/password_reset/email.php | 4 +- sources/app/Template/plugin/directory.php | 55 + sources/app/Template/plugin/layout.php | 9 + sources/app/Template/plugin/remove.php | 13 + sources/app/Template/plugin/show.php | 41 + sources/app/Template/plugin/sidebar.php | 11 + sources/app/Template/project/disable.php | 14 - sources/app/Template/project/dropdown.php | 76 +- sources/app/Template/project/duplicate.php | 27 - sources/app/Template/project/enable.php | 14 - sources/app/Template/project/layout.php | 4 +- sources/app/Template/project/remove.php | 14 - sources/app/Template/project/share.php | 19 - sources/app/Template/project/sidebar.php | 72 +- .../show.php} | 5 +- .../app/Template/project_creation/create.php | 15 +- sources/app/Template/project_edit/dates.php | 10 +- .../app/Template/project_edit/description.php | 10 +- sources/app/Template/project_edit/general.php | 12 +- .../Template/project_edit/task_priority.php | 10 +- sources/app/Template/project_file/create.php | 8 +- sources/app/Template/project_file/remove.php | 6 +- .../projects.php => project_gantt/show.php} | 12 +- .../app/Template/project_header/dropdown.php | 68 +- .../app/Template/project_header/search.php | 8 +- sources/app/Template/project_header/views.php | 24 +- .../index.php => project_list/show.php} | 23 +- .../Template/project_overview/attachments.php | 4 +- .../Template/project_overview/description.php | 4 +- .../app/Template/project_overview/files.php | 8 +- .../app/Template/project_overview/images.php | 8 +- .../Template/project_overview/information.php | 6 +- .../app/Template/project_overview/show.php | 2 +- .../app/Template/project_permission/index.php | 18 +- .../app/Template/project_status/disable.php | 14 + .../app/Template/project_status/enable.php | 14 + .../app/Template/project_status/remove.php | 14 + sources/app/Template/project_tag/create.php | 16 + sources/app/Template/project_tag/edit.php | 17 + sources/app/Template/project_tag/index.php | 31 + sources/app/Template/project_tag/remove.php | 15 + sources/app/Template/project_user/sidebar.php | 30 - .../layout.php | 8 +- .../roles.php | 8 +- .../project_user_overview/sidebar.php | 30 + .../tasks.php | 6 +- .../tooltip_users.php | 12 +- .../app/Template/project_view/duplicate.php | 29 + .../integrations.php | 4 +- .../notifications.php | 6 +- sources/app/Template/project_view/share.php | 18 + .../{project => project_view}/show.php | 12 +- sources/app/Template/search/activity.php | 39 + sources/app/Template/search/index.php | 6 +- sources/app/Template/search/results.php | 8 +- sources/app/Template/subtask/create.php | 4 +- sources/app/Template/subtask/edit.php | 4 +- sources/app/Template/subtask/menu.php | 10 +- sources/app/Template/subtask/remove.php | 17 +- sources/app/Template/subtask/table.php | 6 +- .../app/Template/subtask_converter/show.php | 20 + .../{popover.php => show.php} | 6 +- sources/app/Template/swimlane/create.php | 4 +- sources/app/Template/swimlane/edit.php | 6 +- .../app/Template/swimlane/edit_default.php | 4 +- sources/app/Template/swimlane/index.php | 2 +- sources/app/Template/swimlane/remove.php | 6 +- sources/app/Template/swimlane/table.php | 18 +- sources/app/Template/tag/create.php | 16 + sources/app/Template/tag/edit.php | 17 + sources/app/Template/tag/index.php | 31 + sources/app/Template/tag/remove.php | 15 + sources/app/Template/task/analytics.php | 3 - sources/app/Template/task/color_picker.php | 11 - sources/app/Template/task/description.php | 26 +- sources/app/Template/task/details.php | 281 +- sources/app/Template/task/dropdown.php | 40 +- sources/app/Template/task/layout.php | 14 +- sources/app/Template/task/public.php | 7 +- sources/app/Template/task/remove.php | 15 - sources/app/Template/task/show.php | 7 +- sources/app/Template/task/sidebar.php | 62 +- .../Template/task/time_tracking_details.php | 6 +- sources/app/Template/task/transitions.php | 4 +- sources/app/Template/task_bulk/show.php | 24 + sources/app/Template/task_creation/form.php | 53 - sources/app/Template/task_creation/show.php | 49 + .../app/Template/task_duplication/copy.php | 8 +- .../Template/task_duplication/duplicate.php | 6 +- .../app/Template/task_duplication/move.php | 8 +- .../Template/task_external_link/create.php | 6 +- .../app/Template/task_external_link/edit.php | 6 +- .../app/Template/task_external_link/find.php | 6 +- .../Template/task_external_link/remove.php | 6 +- .../app/Template/task_external_link/table.php | 8 +- sources/app/Template/task_file/create.php | 8 +- sources/app/Template/task_file/files.php | 10 +- sources/app/Template/task_file/images.php | 10 +- sources/app/Template/task_file/remove.php | 6 +- sources/app/Template/task_file/screenshot.php | 6 +- .../project.php => task_gantt/show.php} | 12 +- .../app/Template/task_gantt_creation/show.php | 46 + .../task_import/{step1.php => show.php} | 4 +- sources/app/Template/task_import/sidebar.php | 9 + .../Template/task_internal_link/create.php | 8 +- .../app/Template/task_internal_link/edit.php | 8 +- .../Template/task_internal_link/remove.php | 6 +- .../app/Template/task_internal_link/table.php | 12 +- .../Template/{listing => task_list}/show.php | 10 +- .../task_modification/edit_description.php | 27 - .../Template/task_modification/edit_task.php | 35 - .../app/Template/task_modification/show.php | 44 + sources/app/Template/task_recurrence/edit.php | 10 +- sources/app/Template/task_recurrence/info.php | 10 +- sources/app/Template/task_status/close.php | 6 +- sources/app/Template/task_status/open.php | 6 +- .../app/Template/task_suppression/remove.php | 15 + sources/app/Template/twofactor/check.php | 4 +- sources/app/Template/twofactor/disable.php | 6 +- sources/app/Template/twofactor/index.php | 2 +- sources/app/Template/twofactor/show.php | 4 +- sources/app/Template/user/dropdown.php | 27 - sources/app/Template/user/layout.php | 19 - sources/app/Template/user/share.php | 18 - sources/app/Template/user/sidebar.php | 83 - .../local.php} | 32 +- .../remote.php} | 46 +- .../authentication.php | 7 +- .../{user => user_credential}/password.php | 11 +- sources/app/Template/user_import/show.php | 41 + sources/app/Template/user_import/step1.php | 46 - sources/app/Template/user_list/dropdown.php | 27 + .../{user/index.php => user_list/show.php} | 19 +- .../edit.php => user_modification/show.php} | 6 +- sources/app/Template/user_status/disable.php | 4 +- sources/app/Template/user_status/enable.php | 4 +- sources/app/Template/user_status/remove.php | 4 +- .../Template/{user => user_view}/external.php | 0 .../{user => user_view}/integrations.php | 2 +- .../app/Template/{user => user_view}/last.php | 0 sources/app/Template/user_view/layout.php | 19 + .../{user => user_view}/notifications.php | 6 +- .../{user => user_view}/password_reset.php | 0 .../Template/{user => user_view}/profile.php | 0 .../Template/{user => user_view}/sessions.php | 2 +- sources/app/Template/user_view/share.php | 15 + .../app/Template/{user => user_view}/show.php | 8 +- sources/app/Template/user_view/sidebar.php | 83 + .../{user => user_view}/timesheet.php | 6 +- .../app/User/Avatar/AvatarFileProvider.php | 2 +- sources/app/User/Avatar/GravatarProvider.php | 2 +- .../app/User/Avatar/LetterAvatarProvider.php | 8 +- sources/app/User/LdapUserProvider.php | 48 +- sources/app/User/ReverseProxyUserProvider.php | 21 +- sources/app/Validator/ActionValidator.php | 4 +- sources/app/Validator/AuthValidator.php | 8 +- .../Validator/{Base.php => BaseValidator.php} | 5 +- sources/app/Validator/CategoryValidator.php | 4 +- sources/app/Validator/ColumnValidator.php | 4 +- sources/app/Validator/CommentValidator.php | 4 +- sources/app/Validator/CurrencyValidator.php | 4 +- .../app/Validator/CustomFilterValidator.php | 4 +- .../app/Validator/ExternalLinkValidator.php | 4 +- sources/app/Validator/GroupValidator.php | 8 +- sources/app/Validator/LinkValidator.php | 10 +- .../app/Validator/PasswordResetValidator.php | 6 +- sources/app/Validator/ProjectValidator.php | 16 +- sources/app/Validator/SubtaskValidator.php | 4 +- sources/app/Validator/SwimlaneValidator.php | 4 +- sources/app/Validator/TagValidator.php | 76 + sources/app/Validator/TaskLinkValidator.php | 8 +- sources/app/Validator/TaskValidator.php | 81 +- sources/app/Validator/UserValidator.php | 8 +- sources/app/check_setup.php | 4 +- sources/app/common.php | 64 +- sources/app/constants.php | 48 +- sources/app/functions.php | 76 + sources/assets/css/app.css | 27 - sources/assets/css/app.min.css | 1 + sources/assets/css/chosen-sprite.png | Bin 0 -> 538 bytes sources/assets/css/chosen-sprite@2x.png | Bin 0 -> 738 bytes .../css/images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 212 -> 180 bytes .../images/ui-bg_flat_75_ffffff_40x100.png | Bin 208 -> 178 bytes .../images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 335 -> 120 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 207 -> 105 bytes .../images/ui-bg_glass_75_dadada_1x400.png | Bin 262 -> 111 bytes .../images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 262 -> 110 bytes .../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 332 -> 119 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 280 -> 101 bytes .../css/images/ui-icons_222222_256x240.png | Bin 6922 -> 4369 bytes .../css/images/ui-icons_2e83ff_256x240.png | Bin 4549 -> 4369 bytes .../css/images/ui-icons_444444_256x240.png | Bin 0 -> 6992 bytes .../css/images/ui-icons_454545_256x240.png | Bin 6992 -> 4369 bytes .../css/images/ui-icons_555555_256x240.png | Bin 0 -> 6988 bytes .../css/images/ui-icons_777620_256x240.png | Bin 0 -> 4549 bytes .../css/images/ui-icons_777777_256x240.png | Bin 0 -> 6999 bytes .../css/images/ui-icons_888888_256x240.png | Bin 6999 -> 4369 bytes .../css/images/ui-icons_cc0000_256x240.png | Bin 0 -> 4549 bytes .../css/images/ui-icons_cd0a0a_256x240.png | Bin 4549 -> 4369 bytes .../css/images/ui-icons_ffffff_256x240.png | Bin 0 -> 6299 bytes sources/assets/css/print.css | 27 - sources/assets/css/print.min.css | 1 + sources/assets/css/src/board.css | 13 - sources/assets/css/src/form.css | 35 +- sources/assets/css/src/header.css | 6 +- sources/assets/css/src/sidebar.css | 24 +- sources/assets/css/src/task.css | 63 +- sources/assets/css/src/tooltip.css | 4 + sources/assets/css/vendor.min.css | 481 ++ sources/assets/css/vendor/c3.min.css | 1 - sources/assets/css/vendor/chosen.min.css | 3 - .../assets/css/vendor/font-awesome.min.css | 4 - .../assets/css/vendor/fullcalendar.min.css | 5 - .../vendor/jquery-ui-timepicker-addon.min.css | 5 - sources/assets/css/vendor/jquery-ui.min.css | 7 - .../assets/css/vendor/jquery-ui.theme.min.css | 5 - sources/assets/css/vendor/simplemde.min.css | 7 - sources/assets/fonts/FontAwesome.otf | Bin 109688 -> 124988 bytes sources/assets/fonts/fontawesome-webfont.eot | Bin 70807 -> 76518 bytes sources/assets/fonts/fontawesome-webfont.svg | 58 +- sources/assets/fonts/fontawesome-webfont.ttf | Bin 142072 -> 152796 bytes sources/assets/fonts/fontawesome-webfont.woff | Bin 83588 -> 90412 bytes .../assets/fonts/fontawesome-webfont.woff2 | Bin 66624 -> 71896 bytes sources/assets/img/chosen-sprite.png | Bin 646 -> 0 bytes sources/assets/img/chosen-sprite@2x.png | Bin 872 -> 0 bytes sources/assets/js/app.js | 67 - sources/assets/js/app.min.js | 2 + sources/assets/js/src/App.js | 36 +- sources/assets/js/src/BoardDragAndDrop.js | 2 +- .../js/src/{bootstrap.js => Bootstrap.js} | 0 sources/assets/js/src/Gantt.js | 40 +- sources/assets/js/src/Popover.js | 22 +- sources/assets/js/src/Search.js | 10 +- sources/assets/js/src/Task.js | 28 +- sources/assets/js/vendor.min.js | 4244 +++++++++++++++++ sources/assets/js/vendor/c3.min.js | 5 - sources/assets/js/vendor/chosen.jquery.min.js | 2 - sources/assets/js/vendor/d3.v3.min.js | 5 - sources/assets/js/vendor/fullcalendar.min.js | 9 - sources/assets/js/vendor/jquery-1.11.3.min.js | 5 - .../vendor/jquery-ui-timepicker-addon.min.js | 5 - sources/assets/js/vendor/jquery-ui.min.js | 10 - .../js/vendor/jquery.ui.touch-punch.min.js | 11 - sources/assets/js/vendor/lang/ar-ma.js | 1 - sources/assets/js/vendor/lang/ar-sa.js | 1 - sources/assets/js/vendor/lang/ar-tn.js | 1 - sources/assets/js/vendor/lang/ar.js | 1 - sources/assets/js/vendor/lang/bg.js | 1 - sources/assets/js/vendor/lang/ca.js | 1 - sources/assets/js/vendor/lang/cs.js | 1 - sources/assets/js/vendor/lang/da.js | 1 - sources/assets/js/vendor/lang/de-at.js | 1 - sources/assets/js/vendor/lang/de.js | 1 - sources/assets/js/vendor/lang/el.js | 1 - sources/assets/js/vendor/lang/en-au.js | 1 - sources/assets/js/vendor/lang/en-ca.js | 1 - sources/assets/js/vendor/lang/en-gb.js | 1 - sources/assets/js/vendor/lang/es.js | 1 - sources/assets/js/vendor/lang/fa.js | 1 - sources/assets/js/vendor/lang/fi.js | 1 - sources/assets/js/vendor/lang/fr-ca.js | 1 - sources/assets/js/vendor/lang/fr.js | 1 - sources/assets/js/vendor/lang/he.js | 1 - sources/assets/js/vendor/lang/hi.js | 1 - sources/assets/js/vendor/lang/hr.js | 1 - sources/assets/js/vendor/lang/hu.js | 1 - sources/assets/js/vendor/lang/id.js | 1 - sources/assets/js/vendor/lang/is.js | 1 - sources/assets/js/vendor/lang/it.js | 1 - sources/assets/js/vendor/lang/ja.js | 1 - sources/assets/js/vendor/lang/ko.js | 1 - sources/assets/js/vendor/lang/lt.js | 1 - sources/assets/js/vendor/lang/lv.js | 1 - sources/assets/js/vendor/lang/nb.js | 1 - sources/assets/js/vendor/lang/nl.js | 1 - sources/assets/js/vendor/lang/pl.js | 1 - sources/assets/js/vendor/lang/pt-br.js | 1 - sources/assets/js/vendor/lang/pt.js | 1 - sources/assets/js/vendor/lang/ro.js | 1 - sources/assets/js/vendor/lang/ru.js | 1 - sources/assets/js/vendor/lang/sk.js | 1 - sources/assets/js/vendor/lang/sl.js | 1 - sources/assets/js/vendor/lang/sr-cyrl.js | 1 - sources/assets/js/vendor/lang/sr.js | 1 - sources/assets/js/vendor/lang/sv.js | 1 - sources/assets/js/vendor/lang/th.js | 1 - sources/assets/js/vendor/lang/tr.js | 1 - sources/assets/js/vendor/lang/uk.js | 1 - sources/assets/js/vendor/lang/vi.js | 1 - sources/assets/js/vendor/lang/zh-cn.js | 1 - sources/assets/js/vendor/lang/zh-tw.js | 1 - sources/assets/js/vendor/moment.min.js | 7 - .../js/vendor/mousetrap-global-bind.min.js | 1 - sources/assets/js/vendor/mousetrap.min.js | 9 - sources/assets/js/vendor/simplemde.min.js | 14 - sources/config.default.php | 34 +- sources/data/web.config | 6 + sources/doc/2fa.markdown | 4 +- sources/doc/analytics-tasks.markdown | 4 +- sources/doc/analytics.markdown | 12 +- sources/doc/api-authentication.markdown | 53 +- ...api-external-task-link-procedures.markdown | 221 + .../doc/api-group-member-procedures.markdown | 36 + ...api-internal-task-link-procedures.markdown | 187 + sources/doc/api-json-rpc.markdown | 18 +- sources/doc/api-link-procedures.markdown | 185 - .../doc/api-project-file-procedures.markdown | 221 + ...api-project-permission-procedures.markdown | 33 + sources/doc/api-project-procedures.markdown | 59 +- ...-subtask-time-tracking-procedures.markdown | 102 + ...down => api-task-file-procedures.markdown} | 4 +- sources/doc/api-task-procedures.markdown | 61 + .../doc/application-configuration.markdown | 2 +- sources/doc/assets.markdown | 44 +- sources/doc/board-configuration.markdown | 2 +- sources/doc/board-show-hide-columns.markdown | 2 +- sources/doc/calendar-configuration.markdown | 2 +- sources/doc/calendar.markdown | 2 +- sources/doc/centos-installation.markdown | 6 +- sources/doc/cli.markdown | 56 + sources/doc/closing-tasks.markdown | 4 +- sources/doc/config.markdown | 66 +- sources/doc/contributing.markdown | 2 +- sources/doc/creating-tasks.markdown | 23 +- sources/doc/currency-rate.markdown | 4 +- sources/doc/custom-filters.markdown | 4 +- sources/doc/debian-installation.markdown | 6 +- sources/doc/docker.markdown | 14 +- sources/doc/duplicate-move-tasks.markdown | 4 +- sources/doc/email-configuration.markdown | 2 +- sources/doc/env.markdown | 4 +- .../doc/es_ES/calendar-configuration.markdown | 43 + .../doc/es_ES/email-configuration.markdown | 115 + .../es_ES/kanban-vs-todo-and-scrum.markdown | 38 + sources/doc/faq.markdown | 20 + sources/doc/{fr => fr_FR}/2fa.markdown | 2 +- .../{fr => fr_FR}/analytics-tasks.markdown | 4 +- sources/doc/{fr => fr_FR}/analytics.markdown | 12 +- .../application-configuration.markdown | 2 +- .../application-configuration.markup | 4 +- .../{fr => fr_FR}/automatic-actions.markdown | 0 .../board-collapsed-expanded.markdown | 0 .../board-configuration.markdown | 2 +- ...zontal-scrolling-and-compact-view.markdown | 0 .../board-show-hide-columns.markdown | 0 .../calendar-configuration.markdown | 2 +- sources/doc/{fr => fr_FR}/calendar.markdown | 2 +- .../doc/{fr => fr_FR}/closing-tasks.markdown | 4 +- .../create-tasks-by-email.markdown | 6 +- .../{fr => fr_FR}/creating-projects.markdown | 0 .../doc/{fr => fr_FR}/creating-tasks.markdown | 14 +- .../doc/{fr => fr_FR}/currency-rate.markdown | 2 +- .../duplicate-move-tasks.markdown | 4 +- .../{fr => fr_FR}/editing-projects.markdown | 0 .../gantt-chart-projects.markdown | 2 +- .../{fr => fr_FR}/gantt-chart-tasks.markdown | 4 +- sources/doc/{fr => fr_FR}/index.markdown | 1 + .../kanban-vs-todo-and-scrum.markdown | 0 .../{fr => fr_FR}/keyboard-shortcuts.markdown | 0 .../doc/{fr => fr_FR}/link-labels.markdown | 4 +- .../doc/{fr => fr_FR}/notifications.markdown | 6 +- .../project-configuration.markdown | 4 +- .../project-permissions.markdown | 0 .../doc/{fr => fr_FR}/project-types.markdown | 0 .../doc/{fr => fr_FR}/project-views.markdown | 0 .../{fr => fr_FR}/recurring-tasks.markdown | 2 +- sources/doc/fr_FR/removing-projects.markdown | 10 + sources/doc/{fr => fr_FR}/roles.markdown | 0 .../doc/{fr => fr_FR}/screenshots.markdown | 4 +- .../screenshots/automatic-action-creation.png | Bin .../screenshots/board-collapsed-mode.png | Bin .../screenshots/board-compact-mode.png | Bin .../screenshots/board-expanded-mode.png | Bin .../screenshots/board-task-limit.png | Bin .../{fr => fr_FR}/screenshots/board-view.png | Bin .../screenshots/calendar-view.png | Bin .../{fr => fr_FR}/screenshots/gantt-view.png | Bin .../{fr => fr_FR}/screenshots/hide-column.png | Bin .../{fr => fr_FR}/screenshots/list-view.png | Bin .../{fr => fr_FR}/screenshots/new-project.png | Bin .../{fr => fr_FR}/screenshots/new-user.png | Bin .../screenshots/project-disable-sharing.png | Bin .../screenshots/project-edition.png | Bin .../screenshots/project-enable-sharing.png | Bin .../screenshots/project-permissions.png | Bin .../screenshots/project-view.png | Bin .../{fr => fr_FR}/screenshots/show-column.png | Bin .../screenshots/swimlane-configuration.png | Bin .../{fr => fr_FR}/screenshots/swimlanes.png | Bin .../fr_FR/screenshots/task-creation-board.png | Bin 0 -> 3915 bytes .../fr_FR/screenshots/task-creation-form.png | Bin 0 -> 42123 bytes .../{fr => fr_FR}/sharing-projects.markdown | 0 sources/doc/{fr => fr_FR}/subtasks.markdown | 8 +- sources/doc/{fr => fr_FR}/swimlanes.markdown | 0 sources/doc/{fr => fr_FR}/task-links.markdown | 2 +- .../doc/{fr => fr_FR}/time-tracking.markdown | 8 +- .../doc/{fr => fr_FR}/transitions.markdown | 4 +- .../doc/{fr => fr_FR}/usage-examples.markdown | 0 .../{fr => fr_FR}/user-management.markdown | 0 .../doc/{fr => fr_FR}/what-is-kanban.markdown | 0 sources/doc/gantt-chart-projects.markdown | 2 +- sources/doc/gantt-chart-tasks.markdown | 4 +- sources/doc/heroku.markdown | 2 +- sources/doc/ical.markdown | 14 +- sources/doc/index.markdown | 8 + sources/doc/installation.markdown | 34 +- sources/doc/ldap-authentication.markdown | 4 +- .../doc/ldap-configuration-examples.markdown | 221 + sources/doc/ldap-group-sync.markdown | 28 +- sources/doc/ldap-parameters.markdown | 3 + sources/doc/ldap-profile-picture.markdown | 27 + sources/doc/link-labels.markdown | 4 +- sources/doc/mysql-configuration.markdown | 25 +- sources/doc/nice-urls.markdown | 18 +- sources/doc/nitrous.markdown | 10 + sources/doc/notifications.markdown | 6 +- sources/doc/performances.markdown | 39 + sources/doc/plugin-directory.markdown | 15 + sources/doc/plugin-hooks.markdown | 29 +- sources/doc/plugin-metadata.markdown | 18 +- sources/doc/plugin-notifications.markdown | 6 +- sources/doc/plugin-registration.markdown | 49 +- sources/doc/project-configuration.markdown | 4 +- sources/doc/project-views.markdown | 2 +- sources/doc/recurring-tasks.markdown | 2 +- sources/doc/removing-projects.markdown | 10 + sources/doc/requirements.markdown | 31 +- sources/doc/rss.markdown | 4 +- sources/doc/screenshots.markdown | 4 +- sources/doc/screenshots/project-remove.png | Bin 0 -> 4330 bytes sources/doc/screenshots/tags-board.png | Bin 0 -> 7405 bytes sources/doc/screenshots/tags-global.png | Bin 0 -> 9617 bytes sources/doc/screenshots/tags-projects.png | Bin 0 -> 13718 bytes sources/doc/screenshots/tags-search.png | Bin 0 -> 3904 bytes sources/doc/screenshots/tags-task.png | Bin 0 -> 4766 bytes .../doc/screenshots/task-creation-board.png | Bin 0 -> 10242 bytes .../doc/screenshots/task-creation-form.png | Bin 0 -> 34726 bytes sources/doc/search.markdown | 107 +- sources/doc/subtasks.markdown | 8 +- sources/doc/suse-installation.markdown | 2 +- sources/doc/syntax-guide.markdown | 8 +- sources/doc/tags.markdown | 28 + sources/doc/task-links.markdown | 2 +- sources/doc/tests.markdown | 62 +- sources/doc/time-tracking.markdown | 8 +- sources/doc/transitions.markdown | 4 +- sources/doc/translations.markdown | 2 +- sources/doc/ubuntu-installation.markdown | 6 +- sources/doc/update.markdown | 5 +- sources/doc/webhooks.markdown | 33 - sources/doc/windows-iis-installation.markdown | 40 +- sources/doc/worker.markdown | 35 + sources/index.php | 4 + sources/jsonrpc.php | 46 +- sources/kanboard | 35 +- sources/vendor/autoload.php | 2 +- .../christian-riesen/base32/src/Base32.php | 8 +- .../otp/src/Otp/GoogleAuthenticator.php | 84 +- sources/vendor/composer/ClassLoader.php | 8 +- sources/vendor/composer/LICENSE | 2 +- sources/vendor/composer/autoload_classmap.php | 526 +- sources/vendor/composer/autoload_files.php | 12 +- sources/vendor/composer/autoload_psr4.php | 2 +- sources/vendor/composer/autoload_real.php | 53 +- sources/vendor/composer/autoload_static.php | 850 ++++ sources/vendor/composer/installed.json | 222 +- sources/vendor/eluceo/ical/.php_cs | 26 + sources/vendor/eluceo/ical/CHANGELOG.md | 31 + sources/vendor/eluceo/ical/UPGRADE.md | 7 +- .../vendor/eluceo/ical/examples/example7.php | 33 + sources/vendor/eluceo/ical/phpunit.xml.dist | 21 + .../eluceo/ical/src/Eluceo/iCal/Component.php | 78 +- .../ical/src/Eluceo/iCal/Component/Alarm.php | 8 +- .../src/Eluceo/iCal/Component/Calendar.php | 43 +- .../ical/src/Eluceo/iCal/Component/Event.php | 135 +- .../src/Eluceo/iCal/Component/Timezone.php | 4 +- .../Eluceo/iCal/Component/TimezoneRule.php | 18 +- .../ical/src/Eluceo/iCal/ParameterBag.php | 9 + .../eluceo/ical/src/Eluceo/iCal/Property.php | 11 +- .../src/Eluceo/iCal/Property/ArrayValue.php | 9 + .../Eluceo/iCal/Property/DateTimeProperty.php | 66 +- .../iCal/Property/DateTimesProperty.php | 41 + .../Eluceo/iCal/Property/Event/Attendees.php | 15 +- .../iCal/Property/Event/Description.php | 66 + .../Eluceo/iCal/Property/Event/Organizer.php | 26 +- .../iCal/Property/Event/RecurrenceId.php | 130 + .../iCal/Property/Event/RecurrenceRule.php | 42 +- .../src/Eluceo/iCal/Property/StringValue.php | 9 + .../Eluceo/iCal/Property/ValueInterface.php | 9 + .../ical/src/Eluceo/iCal/PropertyBag.php | 5 +- .../src/Eluceo/iCal/Util/ComponentUtil.php | 10 +- .../ical/src/Eluceo/iCal/Util/DateUtil.php | 69 + .../Eluceo/iCal/Util/PropertyValueUtil.php | 33 +- .../src/JsonRPC/AccessDeniedException.php | 7 - .../fguillot/json-rpc/src/JsonRPC/Client.php | 380 +- .../Exception/AccessDeniedException.php | 15 + .../AuthenticationFailureException.php | 15 + .../Exception/ConnectionFailureException.php | 15 + .../Exception/InvalidJsonFormatException.php | 15 + .../InvalidJsonRpcFormatException.php | 15 + .../ResponseEncodingFailureException.php | 15 + .../{ => Exception}/ResponseException.php | 7 +- .../Exception/ServerErrorException.php | 15 + .../json-rpc/src/JsonRPC/HttpClient.php | 365 ++ .../src/JsonRPC/MiddlewareHandler.php | 114 + .../src/JsonRPC/MiddlewareInterface.php | 27 + .../json-rpc/src/JsonRPC/ProcedureHandler.php | 264 + .../JsonRPC/Request/BatchRequestParser.php | 55 + .../src/JsonRPC/Request/RequestBuilder.php | 129 + .../src/JsonRPC/Request/RequestParser.php | 200 + .../src/JsonRPC/Response/ResponseBuilder.php | 324 ++ .../src/JsonRPC/Response/ResponseParser.php | 154 + .../fguillot/json-rpc/src/JsonRPC/Server.php | 660 +-- .../src/JsonRPC/Validator/HostValidator.php | 30 + .../Validator/JsonEncodingValidator.php | 44 + .../JsonRPC/Validator/JsonFormatValidator.php | 30 + .../JsonRPC/Validator/RpcFormatValidator.php | 35 + .../src/JsonRPC/Validator/UserValidator.php | 21 + .../picodb/lib/PicoDb/Builder/BaseBuilder.php | 86 + .../ConditionBuilder.php} | 24 +- .../lib/PicoDb/Builder/InsertBuilder.php | 36 + .../lib/PicoDb/Builder/UpdateBuilder.php | 56 + .../fguillot/picodb/lib/PicoDb/Database.php | 198 +- .../picodb/lib/PicoDb/Driver/Base.php | 9 +- .../picodb/lib/PicoDb/Driver/Mssql.php | 1 + .../picodb/lib/PicoDb/Driver/Mysql.php | 54 +- .../picodb/lib/PicoDb/Driver/Postgres.php | 5 +- .../picodb/lib/PicoDb/Driver/Sqlite.php | 7 +- .../picodb/lib/PicoDb/DriverFactory.php | 45 + .../fguillot/picodb/lib/PicoDb/Hashtable.php | 13 +- .../picodb/lib/PicoDb/LargeObject.php | 167 + .../picodb/lib/PicoDb/SQLException.php | 3 +- .../fguillot/picodb/lib/PicoDb/Schema.php | 37 +- .../picodb/lib/PicoDb/StatementHandler.php | 353 ++ .../fguillot/picodb/lib/PicoDb/Table.php | 198 +- .../fguillot/picodb/lib/PicoDb/UrlParser.php | 93 + sources/vendor/fguillot/simple-queue/LICENSE | 21 + .../src/Adapter/AmqpQueueAdapter.php | 138 + .../src/Adapter/AwsSqsQueueAdapter.php | 150 + .../src/Adapter/BeanstalkQueueAdapter.php | 120 + .../src/Adapter/DisqueQueueAdapter.php | 109 + .../src/Adapter/MemoryQueueAdapter.php | 100 + .../src/Exception/NotSupportedException.php | 14 + .../vendor/fguillot/simple-queue/src/Job.php | 98 + .../fguillot/simple-queue/src/Queue.php | 92 + .../src/QueueAdapterInterface.php | 58 + .../SimpleValidator/Validators/NotEmpty.php | 15 + .../simpleLogger/src/SimpleLogger/Base.php | 13 + .../simpleLogger/src/SimpleLogger/File.php | 12 +- .../simpleLogger/src/SimpleLogger/Logger.php | 8 +- .../simpleLogger/src/SimpleLogger/Stderr.php | 25 + .../simpleLogger/src/SimpleLogger/Stdout.php | 25 + .../simpleLogger/src/SimpleLogger/Syslog.php | 15 +- .../paragonie/random_compat/CHANGELOG.md | 6 + .../random_compat/lib/byte_safe_strings.php | 8 + .../paragonie/random_compat/lib/random.php | 6 +- .../vendor/swiftmailer/swiftmailer/CHANGES | 15 + .../vendor/swiftmailer/swiftmailer/LICENSE | 2 +- sources/vendor/swiftmailer/swiftmailer/README | 1 - .../vendor/swiftmailer/swiftmailer/VERSION | 2 +- .../AbstractFilterableInputStream.php | 2 + .../Swift/ByteStream/ArrayByteStream.php | 4 +- .../Swift/ByteStream/FileByteStream.php | 4 +- .../GenericFixedWidthReader.php | 2 +- .../Swift/CharacterReader/UsAsciiReader.php | 4 +- .../Swift/CharacterReader/Utf8Reader.php | 5 +- .../CharacterStream/NgCharacterStream.php | 12 +- .../lib/classes/Swift/DependencyContainer.php | 8 +- .../lib/classes/Swift/Encoder/QpEncoder.php | 15 +- .../lib/classes/Swift/FileSpool.php | 4 +- .../classes/Swift/KeyCache/DiskKeyCache.php | 5 +- .../swiftmailer/lib/classes/Swift/Mailer.php | 2 +- .../lib/classes/Swift/MemorySpool.php | 30 +- .../lib/classes/Swift/Mime/Attachment.php | 8 +- .../Mime/ContentEncoder/QpContentEncoder.php | 15 +- .../ContentEncoder/QpContentEncoderProxy.php | 1 + .../lib/classes/Swift/Mime/Grammar.php | 8 +- .../Mime/Headers/ParameterizedHeader.php | 4 +- .../lib/classes/Swift/Mime/MimePart.php | 6 +- .../Swift/Mime/SimpleHeaderFactory.php | 7 +- .../classes/Swift/Mime/SimpleHeaderSet.php | 15 +- .../lib/classes/Swift/Mime/SimpleMessage.php | 2 +- .../classes/Swift/Mime/SimpleMimeEntity.php | 70 +- .../classes/Swift/Plugins/DecoratorPlugin.php | 7 +- .../classes/Swift/Plugins/ReporterPlugin.php | 18 +- .../classes/Swift/Plugins/ThrottlerPlugin.php | 4 +- .../lib/classes/Swift/Signers/DKIMSigner.php | 24 +- .../classes/Swift/Signers/DomainKeySigner.php | 22 +- .../classes/Swift/Signers/OpenDKIMSigner.php | 17 +- .../lib/classes/Swift/Signers/SMimeSigner.php | 9 +- .../Swift/Transport/AbstractSmtpTransport.php | 18 +- .../Esmtp/Auth/NTLMAuthenticator.php | 4 +- .../Swift/Transport/Esmtp/AuthHandler.php | 2 +- .../Swift/Transport/EsmtpTransport.php | 29 +- .../Swift/Transport/FailoverTransport.php | 11 +- .../Swift/Transport/LoadBalancedTransport.php | 19 +- .../classes/Swift/Transport/MailTransport.php | 22 +- .../Swift/Transport/SendmailTransport.php | 1 + .../Swift/Transport/SimpleMailInvoker.php | 4 +- .../classes/Swift/Transport/StreamBuffer.php | 6 +- .../swiftmailer/swiftmailer/phpunit.xml.dist | 2 +- .../symfony/console/Command/Command.php | 9 +- .../vendor/symfony/console/Helper/Helper.php | 30 +- .../symfony/console/Helper/ProgressBar.php | 38 +- .../console/Helper/ProgressIndicator.php | 10 +- .../vendor/symfony/console/Helper/Table.php | 39 +- .../symfony/console/Output/StreamOutput.php | 10 +- .../symfony/console/Style/SymfonyStyle.php | 43 +- .../symfony/console/Tests/ApplicationTest.php | 27 + .../console/Tests/Command/CommandTest.php | 10 + .../Style/SymfonyStyle/command/command_10.php | 17 + .../Style/SymfonyStyle/command/command_11.php | 13 + .../Style/SymfonyStyle/command/command_8.php | 26 + .../Style/SymfonyStyle/command/command_9.php | 11 + .../Style/SymfonyStyle/output/output_10.txt | 7 + .../Style/SymfonyStyle/output/output_11.txt | 6 + .../Style/SymfonyStyle/output/output_5.txt | 7 +- .../Style/SymfonyStyle/output/output_8.txt | 9 + .../Style/SymfonyStyle/output/output_9.txt | 5 + .../console/Tests/Helper/HelperTest.php | 54 + .../console/Tests/Helper/ProgressBarTest.php | 18 +- .../console/Tests/Helper/TableTest.php | 74 +- .../console/Tests/Style/SymfonyStyleTest.php | 2 +- .../ContainerAwareEventDispatcher.php | 10 - .../Debug/TraceableEventDispatcher.php | 43 +- .../event-dispatcher/EventDispatcher.php | 27 +- .../symfony/event-dispatcher/GenericEvent.php | 8 +- .../ImmutableEventDispatcher.php | 8 - .../Tests/AbstractEventDispatcherTest.php | 14 - .../ContainerAwareEventDispatcherTest.php | 4 - .../Debug/TraceableEventDispatcherTest.php | 41 +- .../symfony/polyfill-mbstring/Mbstring.php | 4 +- .../Resources/unidata/lowerCase.php | 1101 +++++ .../Resources/unidata/lowerCase.ser | 1 - .../Resources/unidata/upperCase.php | 1109 +++++ .../Resources/unidata/upperCase.ser | 1 - sources/web.config | 15 +- 1229 files changed, 39417 insertions(+), 16044 deletions(-) create mode 100644 sources/.htaccess create mode 100644 sources/app/Action/TaskAssignColorPriority.php create mode 100644 sources/app/Api/Authorization/ActionAuthorization.php create mode 100644 sources/app/Api/Authorization/CategoryAuthorization.php create mode 100644 sources/app/Api/Authorization/ColumnAuthorization.php create mode 100644 sources/app/Api/Authorization/CommentAuthorization.php create mode 100644 sources/app/Api/Authorization/ProcedureAuthorization.php create mode 100644 sources/app/Api/Authorization/ProjectAuthorization.php create mode 100644 sources/app/Api/Authorization/SubtaskAuthorization.php create mode 100644 sources/app/Api/Authorization/TaskAuthorization.php create mode 100644 sources/app/Api/Authorization/TaskFileAuthorization.php create mode 100644 sources/app/Api/Authorization/TaskLinkAuthorization.php create mode 100644 sources/app/Api/Authorization/UserAuthorization.php delete mode 100644 sources/app/Api/Base.php delete mode 100644 sources/app/Api/Board.php delete mode 100644 sources/app/Api/Category.php delete mode 100644 sources/app/Api/Column.php delete mode 100644 sources/app/Api/Comment.php delete mode 100644 sources/app/Api/File.php delete mode 100644 sources/app/Api/GroupMember.php delete mode 100644 sources/app/Api/Me.php rename sources/app/Api/{Auth.php => Middleware/AuthenticationMiddleware.php} (51%) rename sources/app/Api/{Action.php => Procedure/ActionProcedure.php} (70%) rename sources/app/Api/{App.php => Procedure/AppProcedure.php} (63%) create mode 100644 sources/app/Api/Procedure/BaseProcedure.php create mode 100644 sources/app/Api/Procedure/BoardProcedure.php create mode 100644 sources/app/Api/Procedure/CategoryProcedure.php create mode 100644 sources/app/Api/Procedure/ColumnProcedure.php create mode 100644 sources/app/Api/Procedure/CommentProcedure.php create mode 100644 sources/app/Api/Procedure/GroupMemberProcedure.php rename sources/app/Api/{Group.php => Procedure/GroupProcedure.php} (64%) rename sources/app/Api/{Link.php => Procedure/LinkProcedure.php} (78%) create mode 100644 sources/app/Api/Procedure/MeProcedure.php create mode 100644 sources/app/Api/Procedure/ProjectFileProcedure.php create mode 100644 sources/app/Api/Procedure/ProjectPermissionProcedure.php create mode 100644 sources/app/Api/Procedure/ProjectProcedure.php rename sources/app/Api/{Subtask.php => Procedure/SubtaskProcedure.php} (55%) create mode 100644 sources/app/Api/Procedure/SubtaskTimeTrackingProcedure.php create mode 100644 sources/app/Api/Procedure/SwimlaneProcedure.php create mode 100644 sources/app/Api/Procedure/TaskExternalLinkProcedure.php create mode 100644 sources/app/Api/Procedure/TaskFileProcedure.php rename sources/app/Api/{TaskLink.php => Procedure/TaskLinkProcedure.php} (56%) rename sources/app/Api/{Task.php => Procedure/TaskProcedure.php} (51%) rename sources/app/Api/{User.php => Procedure/UserProcedure.php} (79%) delete mode 100644 sources/app/Api/Project.php delete mode 100644 sources/app/Api/ProjectPermission.php delete mode 100644 sources/app/Api/Swimlane.php delete mode 100644 sources/app/Console/Base.php create mode 100644 sources/app/Console/BaseCommand.php rename sources/app/Console/{Cronjob.php => CronjobCommand.php} (95%) rename sources/app/Console/{LocaleComparator.php => LocaleComparatorCommand.php} (97%) rename sources/app/Console/{LocaleSync.php => LocaleSyncCommand.php} (97%) create mode 100644 sources/app/Console/PluginInstallCommand.php create mode 100644 sources/app/Console/PluginUninstallCommand.php create mode 100644 sources/app/Console/PluginUpgradeCommand.php rename sources/app/Console/{ProjectDailyColumnStatsExport.php => ProjectDailyColumnStatsExportCommand.php} (88%) rename sources/app/Console/{ProjectDailyStatsCalculation.php => ProjectDailyStatsCalculationCommand.php} (60%) create mode 100644 sources/app/Console/ResetPasswordCommand.php create mode 100644 sources/app/Console/ResetTwoFactorCommand.php rename sources/app/Console/{SubtaskExport.php => SubtaskExportCommand.php} (95%) rename sources/app/Console/{TaskExport.php => TaskExportCommand.php} (95%) delete mode 100644 sources/app/Console/TaskOverdueNotification.php create mode 100644 sources/app/Console/TaskOverdueNotificationCommand.php rename sources/app/Console/{TaskTrigger.php => TaskTriggerCommand.php} (79%) rename sources/app/Console/{TransitionExport.php => TransitionExportCommand.php} (95%) create mode 100644 sources/app/Console/WorkerCommand.php rename sources/app/Controller/{Action.php => ActionController.php} (60%) rename sources/app/Controller/{ActionCreation.php => ActionCreationController.php} (73%) rename sources/app/Controller/{Activity.php => ActivityController.php} (68%) rename sources/app/Controller/{Analytic.php => AnalyticController.php} (83%) create mode 100644 sources/app/Controller/AppController.php rename sources/app/Controller/{Auth.php => AuthController.php} (61%) rename sources/app/Controller/{AvatarFile.php => AvatarFileController.php} (74%) delete mode 100644 sources/app/Controller/Base.php create mode 100644 sources/app/Controller/BaseController.php delete mode 100644 sources/app/Controller/Board.php create mode 100644 sources/app/Controller/BoardAjaxController.php delete mode 100644 sources/app/Controller/BoardPopover.php create mode 100644 sources/app/Controller/BoardPopoverController.php rename sources/app/Controller/{BoardTooltip.php => BoardTooltipController.php} (73%) create mode 100644 sources/app/Controller/BoardViewController.php rename sources/app/Controller/{Calendar.php => CalendarController.php} (50%) rename sources/app/Controller/{Captcha.php => CaptchaController.php} (74%) rename sources/app/Controller/{Category.php => CategoryController.php} (68%) rename sources/app/Controller/{Column.php => ColumnController.php} (70%) rename sources/app/Controller/{Comment.php => CommentController.php} (70%) rename sources/app/Controller/{Config.php => ConfigController.php} (59%) rename sources/app/Controller/{Currency.php => CurrencyController.php} (66%) rename sources/app/Controller/{Customfilter.php => CustomFilterController.php} (66%) rename sources/app/Controller/{App.php => DashboardController.php} (74%) delete mode 100644 sources/app/Controller/Doc.php create mode 100644 sources/app/Controller/DocumentationController.php rename sources/app/Controller/{Export.php => ExportController.php} (51%) rename sources/app/Controller/{Feed.php => FeedController.php} (56%) rename sources/app/Controller/{FileViewer.php => FileViewerController.php} (61%) delete mode 100644 sources/app/Controller/Gantt.php delete mode 100644 sources/app/Controller/Group.php create mode 100644 sources/app/Controller/GroupAjaxController.php create mode 100644 sources/app/Controller/GroupCreationController.php delete mode 100644 sources/app/Controller/GroupHelper.php create mode 100644 sources/app/Controller/GroupListController.php create mode 100644 sources/app/Controller/GroupModificationController.php create mode 100644 sources/app/Controller/ICalendarController.php delete mode 100644 sources/app/Controller/Ical.php rename sources/app/Controller/{Link.php => LinkController.php} (70%) rename sources/app/Controller/{Oauth.php => OAuthController.php} (86%) rename sources/app/Controller/{PasswordReset.php => PasswordResetController.php} (62%) create mode 100644 sources/app/Controller/PluginController.php delete mode 100644 sources/app/Controller/Project.php rename sources/app/Controller/{ActionProject.php => ProjectActionDuplicationController.php} (55%) rename sources/app/Controller/{ProjectCreation.php => ProjectCreationController.php} (81%) rename sources/app/Controller/{ProjectEdit.php => ProjectEditController.php} (78%) rename sources/app/Controller/{ProjectFile.php => ProjectFileController.php} (72%) create mode 100644 sources/app/Controller/ProjectGanttController.php create mode 100644 sources/app/Controller/ProjectListController.php rename sources/app/Controller/{ProjectOverview.php => ProjectOverviewController.php} (53%) rename sources/app/Controller/{ProjectPermission.php => ProjectPermissionController.php} (71%) create mode 100644 sources/app/Controller/ProjectStatusController.php create mode 100644 sources/app/Controller/ProjectTagController.php rename sources/app/Controller/{Projectuser.php => ProjectUserOverviewController.php} (72%) create mode 100644 sources/app/Controller/ProjectViewController.php delete mode 100644 sources/app/Controller/Search.php create mode 100644 sources/app/Controller/SearchController.php rename sources/app/Controller/{Subtask.php => SubtaskController.php} (67%) create mode 100644 sources/app/Controller/SubtaskConverterController.php rename sources/app/Controller/{SubtaskRestriction.php => SubtaskRestrictionController.php} (66%) rename sources/app/Controller/{SubtaskStatus.php => SubtaskStatusController.php} (73%) rename sources/app/Controller/{Swimlane.php => SwimlaneController.php} (66%) create mode 100644 sources/app/Controller/TagController.php delete mode 100644 sources/app/Controller/Task.php create mode 100644 sources/app/Controller/TaskAjaxController.php create mode 100644 sources/app/Controller/TaskBulkController.php rename sources/app/Controller/{Taskcreation.php => TaskCreationController.php} (67%) rename sources/app/Controller/{Taskduplication.php => TaskDuplicationController.php} (65%) rename sources/app/Controller/{TaskExternalLink.php => TaskExternalLinkController.php} (71%) rename sources/app/Controller/{TaskFile.php => TaskFileController.php} (60%) create mode 100644 sources/app/Controller/TaskGanttController.php create mode 100644 sources/app/Controller/TaskGanttCreationController.php delete mode 100644 sources/app/Controller/TaskHelper.php delete mode 100644 sources/app/Controller/TaskImport.php create mode 100644 sources/app/Controller/TaskImportController.php rename sources/app/Controller/{TaskInternalLink.php => TaskInternalLinkController.php} (65%) rename sources/app/Controller/{Listing.php => TaskListController.php} (57%) create mode 100644 sources/app/Controller/TaskModificationController.php create mode 100644 sources/app/Controller/TaskPopoverController.php rename sources/app/Controller/{TaskRecurrence.php => TaskRecurrenceController.php} (53%) rename sources/app/Controller/{Taskstatus.php => TaskStatusController.php} (79%) create mode 100644 sources/app/Controller/TaskSuppressionController.php create mode 100644 sources/app/Controller/TaskViewController.php delete mode 100644 sources/app/Controller/Taskmodification.php rename sources/app/Controller/{Twofactor.php => TwoFactorController.php} (81%) delete mode 100644 sources/app/Controller/User.php create mode 100644 sources/app/Controller/UserAjaxController.php create mode 100644 sources/app/Controller/UserCreationController.php create mode 100644 sources/app/Controller/UserCredentialController.php delete mode 100644 sources/app/Controller/UserHelper.php rename sources/app/Controller/{UserImport.php => UserImportController.php} (61%) create mode 100644 sources/app/Controller/UserListController.php create mode 100644 sources/app/Controller/UserModificationController.php rename sources/app/Controller/{UserStatus.php => UserStatusController.php} (79%) create mode 100644 sources/app/Controller/UserViewController.php delete mode 100644 sources/app/Controller/WebNotification.php create mode 100644 sources/app/Controller/WebNotificationController.php delete mode 100644 sources/app/Controller/Webhook.php create mode 100644 sources/app/Core/Controller/AccessForbiddenException.php create mode 100644 sources/app/Core/Controller/BaseException.php create mode 100644 sources/app/Core/Controller/BaseMiddleware.php create mode 100644 sources/app/Core/Controller/PageNotFoundException.php create mode 100644 sources/app/Core/Controller/Runner.php create mode 100644 sources/app/Core/Filter/CriteriaInterface.php create mode 100644 sources/app/Core/Filter/FilterInterface.php create mode 100644 sources/app/Core/Filter/FormatterInterface.php create mode 100644 sources/app/Core/Filter/Lexer.php create mode 100644 sources/app/Core/Filter/LexerBuilder.php create mode 100644 sources/app/Core/Filter/OrCriteria.php create mode 100644 sources/app/Core/Filter/QueryBuilder.php delete mode 100644 sources/app/Core/Lexer.php rename sources/app/{ => Core}/Notification/NotificationInterface.php (89%) create mode 100644 sources/app/Core/Plugin/Directory.php create mode 100644 sources/app/Core/Plugin/Installer.php create mode 100644 sources/app/Core/Plugin/PluginInstallerException.php create mode 100644 sources/app/Core/Plugin/SchemaHandler.php create mode 100644 sources/app/Core/Queue/JobHandler.php create mode 100644 sources/app/Core/Queue/QueueManager.php create mode 100644 sources/app/Event/UserProfileSyncEvent.php create mode 100644 sources/app/Filter/BaseDateFilter.php create mode 100644 sources/app/Filter/BaseFilter.php create mode 100644 sources/app/Filter/ProjectActivityCreationDateFilter.php create mode 100644 sources/app/Filter/ProjectActivityCreatorFilter.php create mode 100644 sources/app/Filter/ProjectActivityProjectIdFilter.php create mode 100644 sources/app/Filter/ProjectActivityProjectIdsFilter.php create mode 100644 sources/app/Filter/ProjectActivityProjectNameFilter.php create mode 100644 sources/app/Filter/ProjectActivityTaskIdFilter.php create mode 100644 sources/app/Filter/ProjectActivityTaskStatusFilter.php create mode 100644 sources/app/Filter/ProjectActivityTaskTitleFilter.php create mode 100644 sources/app/Filter/ProjectGroupRoleProjectFilter.php create mode 100644 sources/app/Filter/ProjectGroupRoleUsernameFilter.php create mode 100644 sources/app/Filter/ProjectIdsFilter.php create mode 100644 sources/app/Filter/ProjectStatusFilter.php create mode 100644 sources/app/Filter/ProjectTypeFilter.php create mode 100644 sources/app/Filter/ProjectUserRoleProjectFilter.php create mode 100644 sources/app/Filter/ProjectUserRoleUsernameFilter.php create mode 100644 sources/app/Filter/TaskAssigneeFilter.php create mode 100644 sources/app/Filter/TaskCategoryFilter.php create mode 100644 sources/app/Filter/TaskColorFilter.php create mode 100644 sources/app/Filter/TaskColumnFilter.php create mode 100644 sources/app/Filter/TaskCommentFilter.php create mode 100644 sources/app/Filter/TaskCompletionDateFilter.php create mode 100644 sources/app/Filter/TaskCreationDateFilter.php create mode 100644 sources/app/Filter/TaskCreatorFilter.php create mode 100644 sources/app/Filter/TaskDescriptionFilter.php create mode 100644 sources/app/Filter/TaskDueDateFilter.php create mode 100644 sources/app/Filter/TaskDueDateRangeFilter.php create mode 100644 sources/app/Filter/TaskIdExclusionFilter.php create mode 100644 sources/app/Filter/TaskIdFilter.php create mode 100644 sources/app/Filter/TaskLinkFilter.php create mode 100644 sources/app/Filter/TaskModificationDateFilter.php create mode 100644 sources/app/Filter/TaskProjectFilter.php create mode 100644 sources/app/Filter/TaskProjectsFilter.php create mode 100644 sources/app/Filter/TaskReferenceFilter.php create mode 100644 sources/app/Filter/TaskStartDateFilter.php create mode 100644 sources/app/Filter/TaskStatusFilter.php create mode 100644 sources/app/Filter/TaskSubtaskAssigneeFilter.php create mode 100644 sources/app/Filter/TaskSwimlaneFilter.php create mode 100644 sources/app/Filter/TaskTagFilter.php create mode 100644 sources/app/Filter/TaskTitleFilter.php create mode 100644 sources/app/Filter/UserNameFilter.php create mode 100644 sources/app/Formatter/BaseFormatter.php rename sources/app/Formatter/{TaskFilterCalendarEvent.php => BaseTaskCalendarFormatter.php} (56%) create mode 100644 sources/app/Formatter/BoardColumnFormatter.php create mode 100644 sources/app/Formatter/BoardFormatter.php create mode 100644 sources/app/Formatter/BoardSwimlaneFormatter.php create mode 100644 sources/app/Formatter/BoardTaskFormatter.php delete mode 100644 sources/app/Formatter/FormatterInterface.php create mode 100644 sources/app/Formatter/ProjectActivityEventFormatter.php create mode 100644 sources/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php create mode 100644 sources/app/Formatter/TaskAutoCompleteFormatter.php create mode 100644 sources/app/Formatter/TaskCalendarFormatter.php delete mode 100644 sources/app/Formatter/TaskFilterAutoCompleteFormatter.php delete mode 100644 sources/app/Formatter/TaskFilterCalendarFormatter.php rename sources/app/Formatter/{TaskFilterGanttFormatter.php => TaskGanttFormatter.php} (69%) rename sources/app/Formatter/{TaskFilterICalendarFormatter.php => TaskICalFormatter.php} (71%) rename sources/app/Formatter/{UserFilterAutoCompleteFormatter.php => UserAutoCompleteFormatter.php} (64%) create mode 100644 sources/app/Helper/CalendarHelper.php create mode 100644 sources/app/Helper/ICalHelper.php create mode 100644 sources/app/Helper/MailHelper.php create mode 100644 sources/app/Helper/ProjectActivityHelper.php create mode 100644 sources/app/Job/BaseJob.php create mode 100644 sources/app/Job/EmailJob.php create mode 100644 sources/app/Job/HttpAsyncJob.php create mode 100644 sources/app/Job/NotificationJob.php create mode 100644 sources/app/Job/ProjectMetricJob.php create mode 100644 sources/app/Middleware/ApplicationAuthorizationMiddleware.php create mode 100644 sources/app/Middleware/AuthenticationMiddleware.php create mode 100644 sources/app/Middleware/BootstrapMiddleware.php create mode 100644 sources/app/Middleware/PostAuthenticationMiddleware.php create mode 100644 sources/app/Middleware/ProjectAuthorizationMiddleware.php rename sources/app/Model/{Action.php => ActionModel.php} (79%) rename sources/app/Model/{ActionParameter.php => ActionParameterModel.php} (87%) rename sources/app/Model/{AvatarFile.php => AvatarFileModel.php} (57%) delete mode 100644 sources/app/Model/Base.php rename sources/app/Model/{Board.php => BoardModel.php} (51%) rename sources/app/Model/{Category.php => CategoryModel.php} (88%) rename sources/app/Model/{Color.php => ColorModel.php} (97%) rename sources/app/Model/{Column.php => ColumnModel.php} (92%) rename sources/app/Model/{Comment.php => CommentModel.php} (73%) delete mode 100644 sources/app/Model/Config.php create mode 100644 sources/app/Model/ConfigModel.php rename sources/app/Model/{Currency.php => CurrencyModel.php} (94%) rename sources/app/Model/{CustomFilter.php => CustomFilterModel.php} (86%) rename sources/app/Model/{File.php => FileModel.php} (77%) rename sources/app/Model/{GroupMember.php => GroupMemberModel.php} (75%) rename sources/app/Model/{Group.php => GroupModel.php} (92%) create mode 100644 sources/app/Model/LanguageModel.php rename sources/app/Model/{LastLogin.php => LastLoginModel.php} (93%) rename sources/app/Model/{Link.php => LinkModel.php} (98%) rename sources/app/Model/{Metadata.php => MetadataModel.php} (80%) rename sources/app/Model/{Notification.php => NotificationModel.php} (63%) rename sources/app/Model/{NotificationType.php => NotificationTypeModel.php} (92%) rename sources/app/Model/{PasswordReset.php => PasswordResetModel.php} (89%) delete mode 100644 sources/app/Model/ProjectActivity.php create mode 100644 sources/app/Model/ProjectActivityModel.php rename sources/app/Model/{ProjectDailyColumnStats.php => ProjectDailyColumnStatsModel.php} (92%) rename sources/app/Model/{ProjectDailyStats.php => ProjectDailyStatsModel.php} (95%) rename sources/app/Model/{ProjectDuplication.php => ProjectDuplicationModel.php} (74%) delete mode 100644 sources/app/Model/ProjectFile.php create mode 100644 sources/app/Model/ProjectFileModel.php delete mode 100644 sources/app/Model/ProjectGroupRoleFilter.php rename sources/app/Model/{ProjectGroupRole.php => ProjectGroupRoleModel.php} (74%) delete mode 100644 sources/app/Model/ProjectMetadata.php create mode 100644 sources/app/Model/ProjectMetadataModel.php rename sources/app/Model/{Project.php => ProjectModel.php} (88%) rename sources/app/Model/{ProjectNotification.php => ProjectNotificationModel.php} (68%) rename sources/app/Model/{ProjectNotificationType.php => ProjectNotificationTypeModel.php} (93%) rename sources/app/Model/{ProjectPermission.php => ProjectPermissionModel.php} (56%) create mode 100644 sources/app/Model/ProjectTaskDuplicationModel.php create mode 100644 sources/app/Model/ProjectTaskPriorityModel.php delete mode 100644 sources/app/Model/ProjectUserRoleFilter.php rename sources/app/Model/{ProjectUserRole.php => ProjectUserRoleModel.php} (74%) rename sources/app/Model/{RememberMeSession.php => RememberMeSessionModel.php} (97%) rename sources/app/Model/{Setting.php => SettingModel.php} (95%) rename sources/app/Model/{Subtask.php => SubtaskModel.php} (78%) rename sources/app/Model/{SubtaskTimeTracking.php => SubtaskTimeTrackingModel.php} (60%) rename sources/app/Model/{Swimlane.php => SwimlaneModel.php} (90%) create mode 100644 sources/app/Model/TagDuplicationModel.php create mode 100644 sources/app/Model/TagModel.php delete mode 100644 sources/app/Model/Task.php rename sources/app/Model/{TaskAnalytic.php => TaskAnalyticModel.php} (86%) rename sources/app/Model/{TaskCreation.php => TaskCreationModel.php} (57%) delete mode 100644 sources/app/Model/TaskDuplication.php create mode 100644 sources/app/Model/TaskDuplicationModel.php rename sources/app/Model/{TaskExternalLink.php => TaskExternalLinkModel.php} (85%) delete mode 100644 sources/app/Model/TaskFile.php create mode 100644 sources/app/Model/TaskFileModel.php delete mode 100644 sources/app/Model/TaskFilter.php rename sources/app/Model/{TaskFinder.php => TaskFinderModel.php} (53%) rename sources/app/Model/{TaskLink.php => TaskLinkModel.php} (70%) rename sources/app/Model/{TaskMetadata.php => TaskMetadataModel.php} (55%) create mode 100644 sources/app/Model/TaskModel.php rename sources/app/Model/{TaskModification.php => TaskModificationModel.php} (70%) delete mode 100644 sources/app/Model/TaskPermission.php rename sources/app/Model/{TaskPosition.php => TaskPositionModel.php} (87%) create mode 100644 sources/app/Model/TaskProjectDuplicationModel.php create mode 100644 sources/app/Model/TaskProjectMoveModel.php create mode 100644 sources/app/Model/TaskRecurrenceModel.php rename sources/app/Model/{TaskStatus.php => TaskStatusModel.php} (74%) create mode 100644 sources/app/Model/TaskTagModel.php create mode 100644 sources/app/Model/TimezoneModel.php rename sources/app/Model/{Transition.php => TransitionModel.php} (76%) delete mode 100644 sources/app/Model/UserFilter.php rename sources/app/Model/{UserLocking.php => UserLockingModel.php} (86%) rename sources/app/Model/{UserMention.php => UserMentionModel.php} (79%) rename sources/app/Model/{UserMetadata.php => UserMetadataModel.php} (55%) rename sources/app/Model/{User.php => UserModel.php} (88%) rename sources/app/Model/{UserNotificationFilter.php => UserNotificationFilterModel.php} (94%) rename sources/app/Model/{UserNotification.php => UserNotificationModel.php} (60%) rename sources/app/Model/{UserNotificationType.php => UserNotificationTypeModel.php} (92%) rename sources/app/Model/{UserUnreadNotification.php => UserUnreadNotificationModel.php} (72%) rename sources/app/Notification/{ActivityStream.php => ActivityStreamNotification.php} (81%) rename sources/app/Notification/{Mail.php => MailNotification.php} (79%) rename sources/app/Notification/{Web.php => WebNotification.php} (75%) rename sources/app/Notification/{Webhook.php => WebhookNotification.php} (79%) create mode 100644 sources/app/ServiceProvider/ApiProvider.php create mode 100644 sources/app/ServiceProvider/CommandProvider.php create mode 100644 sources/app/ServiceProvider/FilterProvider.php create mode 100644 sources/app/ServiceProvider/MailProvider.php create mode 100644 sources/app/ServiceProvider/QueueProvider.php create mode 100644 sources/app/Subscriber/LdapUserPhotoSubscriber.php create mode 100644 sources/app/Template/activity/filter_dropdown.php delete mode 100644 sources/app/Template/app/calendar.php delete mode 100644 sources/app/Template/app/sidebar.php delete mode 100644 sources/app/Template/board/popover_assignee.php delete mode 100644 sources/app/Template/board/popover_category.php delete mode 100644 sources/app/Template/board/task_menu.php rename sources/app/Template/{board/popover_close_all_tasks_column.php => board_popover/close_all_tasks_column.php} (74%) create mode 100644 sources/app/Template/config/email.php delete mode 100644 sources/app/Template/config/plugins.php rename sources/app/Template/{app => dashboard}/activity.php (100%) create mode 100644 sources/app/Template/dashboard/calendar.php rename sources/app/Template/{app => dashboard}/layout.php (69%) rename sources/app/Template/{app => dashboard}/notifications.php (59%) rename sources/app/Template/{app => dashboard}/projects.php (61%) rename sources/app/Template/{app/overview.php => dashboard/show.php} (60%) create mode 100644 sources/app/Template/dashboard/sidebar.php rename sources/app/Template/{app => dashboard}/subtasks.php (81%) rename sources/app/Template/{app => dashboard}/tasks.php (64%) delete mode 100644 sources/app/Template/gantt/task_creation.php delete mode 100644 sources/app/Template/group/create.php delete mode 100644 sources/app/Template/group/edit.php create mode 100644 sources/app/Template/group_creation/show.php create mode 100644 sources/app/Template/group_modification/show.php create mode 100644 sources/app/Template/plugin/directory.php create mode 100644 sources/app/Template/plugin/layout.php create mode 100644 sources/app/Template/plugin/remove.php create mode 100644 sources/app/Template/plugin/show.php create mode 100644 sources/app/Template/plugin/sidebar.php delete mode 100644 sources/app/Template/project/disable.php delete mode 100644 sources/app/Template/project/duplicate.php delete mode 100644 sources/app/Template/project/enable.php delete mode 100644 sources/app/Template/project/remove.php delete mode 100644 sources/app/Template/project/share.php rename sources/app/Template/{action_project/project.php => project_action_duplication/show.php} (84%) rename sources/app/Template/{gantt/projects.php => project_gantt/show.php} (67%) rename sources/app/Template/{project/index.php => project_list/show.php} (70%) create mode 100644 sources/app/Template/project_status/disable.php create mode 100644 sources/app/Template/project_status/enable.php create mode 100644 sources/app/Template/project_status/remove.php create mode 100644 sources/app/Template/project_tag/create.php create mode 100644 sources/app/Template/project_tag/edit.php create mode 100644 sources/app/Template/project_tag/index.php create mode 100644 sources/app/Template/project_tag/remove.php delete mode 100644 sources/app/Template/project_user/sidebar.php rename sources/app/Template/{project_user => project_user_overview}/layout.php (75%) rename sources/app/Template/{project_user => project_user_overview}/roles.php (76%) create mode 100644 sources/app/Template/project_user_overview/sidebar.php rename sources/app/Template/{project_user => project_user_overview}/tasks.php (83%) rename sources/app/Template/{project_user => project_user_overview}/tooltip_users.php (55%) create mode 100644 sources/app/Template/project_view/duplicate.php rename sources/app/Template/{project => project_view}/integrations.php (74%) rename sources/app/Template/{project => project_view}/notifications.php (67%) create mode 100644 sources/app/Template/project_view/share.php rename sources/app/Template/{project => project_view}/show.php (79%) create mode 100644 sources/app/Template/search/activity.php create mode 100644 sources/app/Template/subtask_converter/show.php rename sources/app/Template/subtask_restriction/{popover.php => show.php} (62%) create mode 100644 sources/app/Template/tag/create.php create mode 100644 sources/app/Template/tag/edit.php create mode 100644 sources/app/Template/tag/index.php create mode 100644 sources/app/Template/tag/remove.php delete mode 100644 sources/app/Template/task/color_picker.php delete mode 100644 sources/app/Template/task/remove.php create mode 100644 sources/app/Template/task_bulk/show.php delete mode 100644 sources/app/Template/task_creation/form.php create mode 100644 sources/app/Template/task_creation/show.php rename sources/app/Template/{gantt/project.php => task_gantt/show.php} (62%) create mode 100644 sources/app/Template/task_gantt_creation/show.php rename sources/app/Template/task_import/{step1.php => show.php} (82%) create mode 100644 sources/app/Template/task_import/sidebar.php rename sources/app/Template/{listing => task_list}/show.php (87%) delete mode 100644 sources/app/Template/task_modification/edit_description.php delete mode 100644 sources/app/Template/task_modification/edit_task.php create mode 100644 sources/app/Template/task_modification/show.php create mode 100644 sources/app/Template/task_suppression/remove.php delete mode 100644 sources/app/Template/user/dropdown.php delete mode 100644 sources/app/Template/user/layout.php delete mode 100644 sources/app/Template/user/share.php delete mode 100644 sources/app/Template/user/sidebar.php rename sources/app/Template/{user/create_local.php => user_creation/local.php} (69%) rename sources/app/Template/{user/create_remote.php => user_creation/remote.php} (59%) rename sources/app/Template/{user => user_credential}/authentication.php (80%) rename sources/app/Template/{user => user_credential}/password.php (57%) create mode 100644 sources/app/Template/user_import/show.php delete mode 100644 sources/app/Template/user_import/step1.php create mode 100644 sources/app/Template/user_list/dropdown.php rename sources/app/Template/{user/index.php => user_list/show.php} (76%) rename sources/app/Template/{user/edit.php => user_modification/show.php} (82%) rename sources/app/Template/{user => user_view}/external.php (100%) rename sources/app/Template/{user => user_view}/integrations.php (72%) rename sources/app/Template/{user => user_view}/last.php (100%) create mode 100644 sources/app/Template/user_view/layout.php rename sources/app/Template/{user => user_view}/notifications.php (74%) rename sources/app/Template/{user => user_view}/password_reset.php (100%) rename sources/app/Template/{user => user_view}/profile.php (100%) rename sources/app/Template/{user => user_view}/sessions.php (85%) create mode 100644 sources/app/Template/user_view/share.php rename sources/app/Template/{user => user_view}/show.php (75%) create mode 100644 sources/app/Template/user_view/sidebar.php rename sources/app/Template/{user => user_view}/timesheet.php (82%) rename sources/app/Validator/{Base.php => BaseValidator.php} (93%) create mode 100644 sources/app/Validator/TagValidator.php delete mode 100644 sources/assets/css/app.css create mode 100644 sources/assets/css/app.min.css create mode 100644 sources/assets/css/chosen-sprite.png create mode 100644 sources/assets/css/chosen-sprite@2x.png create mode 100644 sources/assets/css/images/ui-icons_444444_256x240.png create mode 100644 sources/assets/css/images/ui-icons_555555_256x240.png create mode 100644 sources/assets/css/images/ui-icons_777620_256x240.png create mode 100644 sources/assets/css/images/ui-icons_777777_256x240.png create mode 100644 sources/assets/css/images/ui-icons_cc0000_256x240.png create mode 100644 sources/assets/css/images/ui-icons_ffffff_256x240.png delete mode 100644 sources/assets/css/print.css create mode 100644 sources/assets/css/print.min.css create mode 100644 sources/assets/css/vendor.min.css delete mode 100644 sources/assets/css/vendor/c3.min.css delete mode 100755 sources/assets/css/vendor/chosen.min.css delete mode 100644 sources/assets/css/vendor/font-awesome.min.css delete mode 100644 sources/assets/css/vendor/fullcalendar.min.css delete mode 100644 sources/assets/css/vendor/jquery-ui-timepicker-addon.min.css delete mode 100755 sources/assets/css/vendor/jquery-ui.min.css delete mode 100755 sources/assets/css/vendor/jquery-ui.theme.min.css delete mode 100644 sources/assets/css/vendor/simplemde.min.css delete mode 100644 sources/assets/img/chosen-sprite.png delete mode 100644 sources/assets/img/chosen-sprite@2x.png delete mode 100644 sources/assets/js/app.js create mode 100644 sources/assets/js/app.min.js rename sources/assets/js/src/{bootstrap.js => Bootstrap.js} (100%) create mode 100644 sources/assets/js/vendor.min.js delete mode 100644 sources/assets/js/vendor/c3.min.js delete mode 100644 sources/assets/js/vendor/chosen.jquery.min.js delete mode 100644 sources/assets/js/vendor/d3.v3.min.js delete mode 100644 sources/assets/js/vendor/fullcalendar.min.js delete mode 100644 sources/assets/js/vendor/jquery-1.11.3.min.js delete mode 100644 sources/assets/js/vendor/jquery-ui-timepicker-addon.min.js delete mode 100644 sources/assets/js/vendor/jquery-ui.min.js delete mode 100644 sources/assets/js/vendor/jquery.ui.touch-punch.min.js delete mode 100644 sources/assets/js/vendor/lang/ar-ma.js delete mode 100644 sources/assets/js/vendor/lang/ar-sa.js delete mode 100644 sources/assets/js/vendor/lang/ar-tn.js delete mode 100644 sources/assets/js/vendor/lang/ar.js delete mode 100644 sources/assets/js/vendor/lang/bg.js delete mode 100644 sources/assets/js/vendor/lang/ca.js delete mode 100644 sources/assets/js/vendor/lang/cs.js delete mode 100644 sources/assets/js/vendor/lang/da.js delete mode 100644 sources/assets/js/vendor/lang/de-at.js delete mode 100644 sources/assets/js/vendor/lang/de.js delete mode 100644 sources/assets/js/vendor/lang/el.js delete mode 100644 sources/assets/js/vendor/lang/en-au.js delete mode 100644 sources/assets/js/vendor/lang/en-ca.js delete mode 100644 sources/assets/js/vendor/lang/en-gb.js delete mode 100644 sources/assets/js/vendor/lang/es.js delete mode 100644 sources/assets/js/vendor/lang/fa.js delete mode 100644 sources/assets/js/vendor/lang/fi.js delete mode 100644 sources/assets/js/vendor/lang/fr-ca.js delete mode 100644 sources/assets/js/vendor/lang/fr.js delete mode 100644 sources/assets/js/vendor/lang/he.js delete mode 100644 sources/assets/js/vendor/lang/hi.js delete mode 100644 sources/assets/js/vendor/lang/hr.js delete mode 100644 sources/assets/js/vendor/lang/hu.js delete mode 100644 sources/assets/js/vendor/lang/id.js delete mode 100644 sources/assets/js/vendor/lang/is.js delete mode 100644 sources/assets/js/vendor/lang/it.js delete mode 100644 sources/assets/js/vendor/lang/ja.js delete mode 100644 sources/assets/js/vendor/lang/ko.js delete mode 100644 sources/assets/js/vendor/lang/lt.js delete mode 100644 sources/assets/js/vendor/lang/lv.js delete mode 100644 sources/assets/js/vendor/lang/nb.js delete mode 100644 sources/assets/js/vendor/lang/nl.js delete mode 100644 sources/assets/js/vendor/lang/pl.js delete mode 100644 sources/assets/js/vendor/lang/pt-br.js delete mode 100644 sources/assets/js/vendor/lang/pt.js delete mode 100644 sources/assets/js/vendor/lang/ro.js delete mode 100644 sources/assets/js/vendor/lang/ru.js delete mode 100644 sources/assets/js/vendor/lang/sk.js delete mode 100644 sources/assets/js/vendor/lang/sl.js delete mode 100644 sources/assets/js/vendor/lang/sr-cyrl.js delete mode 100644 sources/assets/js/vendor/lang/sr.js delete mode 100644 sources/assets/js/vendor/lang/sv.js delete mode 100644 sources/assets/js/vendor/lang/th.js delete mode 100644 sources/assets/js/vendor/lang/tr.js delete mode 100644 sources/assets/js/vendor/lang/uk.js delete mode 100644 sources/assets/js/vendor/lang/vi.js delete mode 100644 sources/assets/js/vendor/lang/zh-cn.js delete mode 100644 sources/assets/js/vendor/lang/zh-tw.js delete mode 100644 sources/assets/js/vendor/moment.min.js delete mode 100644 sources/assets/js/vendor/mousetrap-global-bind.min.js delete mode 100644 sources/assets/js/vendor/mousetrap.min.js delete mode 100644 sources/assets/js/vendor/simplemde.min.js create mode 100644 sources/data/web.config create mode 100644 sources/doc/api-external-task-link-procedures.markdown create mode 100644 sources/doc/api-internal-task-link-procedures.markdown create mode 100644 sources/doc/api-project-file-procedures.markdown create mode 100644 sources/doc/api-subtask-time-tracking-procedures.markdown rename sources/doc/{api-file-procedures.markdown => api-task-file-procedures.markdown} (98%) create mode 100644 sources/doc/es_ES/calendar-configuration.markdown create mode 100644 sources/doc/es_ES/email-configuration.markdown create mode 100644 sources/doc/es_ES/kanban-vs-todo-and-scrum.markdown rename sources/doc/{fr => fr_FR}/2fa.markdown (96%) rename sources/doc/{fr => fr_FR}/analytics-tasks.markdown (82%) rename sources/doc/{fr => fr_FR}/analytics.markdown (80%) rename sources/doc/{fr => fr_FR}/application-configuration.markdown (92%) rename sources/doc/{fr => fr_FR}/application-configuration.markup (90%) rename sources/doc/{fr => fr_FR}/automatic-actions.markdown (100%) rename sources/doc/{fr => fr_FR}/board-collapsed-expanded.markdown (100%) rename sources/doc/{fr => fr_FR}/board-configuration.markdown (90%) rename sources/doc/{fr => fr_FR}/board-horizontal-scrolling-and-compact-view.markdown (100%) rename sources/doc/{fr => fr_FR}/board-show-hide-columns.markdown (100%) rename sources/doc/{fr => fr_FR}/calendar-configuration.markdown (94%) rename sources/doc/{fr => fr_FR}/calendar.markdown (92%) rename sources/doc/{fr => fr_FR}/closing-tasks.markdown (76%) rename sources/doc/{fr => fr_FR}/create-tasks-by-email.markdown (91%) rename sources/doc/{fr => fr_FR}/creating-projects.markdown (100%) rename sources/doc/{fr => fr_FR}/creating-tasks.markdown (72%) rename sources/doc/{fr => fr_FR}/currency-rate.markdown (83%) rename sources/doc/{fr => fr_FR}/duplicate-move-tasks.markdown (86%) rename sources/doc/{fr => fr_FR}/editing-projects.markdown (100%) rename sources/doc/{fr => fr_FR}/gantt-chart-projects.markdown (88%) rename sources/doc/{fr => fr_FR}/gantt-chart-tasks.markdown (85%) rename sources/doc/{fr => fr_FR}/index.markdown (97%) rename sources/doc/{fr => fr_FR}/kanban-vs-todo-and-scrum.markdown (100%) rename sources/doc/{fr => fr_FR}/keyboard-shortcuts.markdown (100%) rename sources/doc/{fr => fr_FR}/link-labels.markdown (63%) rename sources/doc/{fr => fr_FR}/notifications.markdown (84%) rename sources/doc/{fr => fr_FR}/project-configuration.markdown (88%) rename sources/doc/{fr => fr_FR}/project-permissions.markdown (100%) rename sources/doc/{fr => fr_FR}/project-types.markdown (100%) rename sources/doc/{fr => fr_FR}/project-views.markdown (100%) rename sources/doc/{fr => fr_FR}/recurring-tasks.markdown (92%) create mode 100644 sources/doc/fr_FR/removing-projects.markdown rename sources/doc/{fr => fr_FR}/roles.markdown (100%) rename sources/doc/{fr => fr_FR}/screenshots.markdown (86%) rename sources/doc/{fr => fr_FR}/screenshots/automatic-action-creation.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/board-collapsed-mode.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/board-compact-mode.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/board-expanded-mode.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/board-task-limit.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/board-view.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/calendar-view.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/gantt-view.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/hide-column.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/list-view.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/new-project.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/new-user.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/project-disable-sharing.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/project-edition.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/project-enable-sharing.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/project-permissions.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/project-view.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/show-column.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/swimlane-configuration.png (100%) rename sources/doc/{fr => fr_FR}/screenshots/swimlanes.png (100%) create mode 100644 sources/doc/fr_FR/screenshots/task-creation-board.png create mode 100644 sources/doc/fr_FR/screenshots/task-creation-form.png rename sources/doc/{fr => fr_FR}/sharing-projects.markdown (100%) rename sources/doc/{fr => fr_FR}/subtasks.markdown (79%) rename sources/doc/{fr => fr_FR}/swimlanes.markdown (100%) rename sources/doc/{fr => fr_FR}/task-links.markdown (90%) rename sources/doc/{fr => fr_FR}/time-tracking.markdown (78%) rename sources/doc/{fr => fr_FR}/transitions.markdown (78%) rename sources/doc/{fr => fr_FR}/usage-examples.markdown (100%) rename sources/doc/{fr => fr_FR}/user-management.markdown (100%) rename sources/doc/{fr => fr_FR}/what-is-kanban.markdown (100%) create mode 100644 sources/doc/ldap-configuration-examples.markdown create mode 100644 sources/doc/ldap-profile-picture.markdown create mode 100644 sources/doc/nitrous.markdown create mode 100644 sources/doc/performances.markdown create mode 100644 sources/doc/plugin-directory.markdown create mode 100644 sources/doc/removing-projects.markdown create mode 100644 sources/doc/screenshots/project-remove.png create mode 100644 sources/doc/screenshots/tags-board.png create mode 100644 sources/doc/screenshots/tags-global.png create mode 100644 sources/doc/screenshots/tags-projects.png create mode 100644 sources/doc/screenshots/tags-search.png create mode 100644 sources/doc/screenshots/tags-task.png create mode 100644 sources/doc/screenshots/task-creation-board.png create mode 100644 sources/doc/screenshots/task-creation-form.png create mode 100644 sources/doc/tags.markdown create mode 100644 sources/doc/worker.markdown create mode 100644 sources/vendor/composer/autoload_static.php create mode 100644 sources/vendor/eluceo/ical/.php_cs create mode 100644 sources/vendor/eluceo/ical/CHANGELOG.md create mode 100644 sources/vendor/eluceo/ical/examples/example7.php create mode 100644 sources/vendor/eluceo/ical/phpunit.xml.dist create mode 100644 sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/DateTimesProperty.php create mode 100644 sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/Description.php create mode 100644 sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/RecurrenceId.php create mode 100644 sources/vendor/eluceo/ical/src/Eluceo/iCal/Util/DateUtil.php delete mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/AccessDeniedException.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Exception/AccessDeniedException.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Exception/AuthenticationFailureException.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Exception/ConnectionFailureException.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Exception/InvalidJsonFormatException.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Exception/InvalidJsonRpcFormatException.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Exception/ResponseEncodingFailureException.php rename sources/vendor/fguillot/json-rpc/src/JsonRPC/{ => Exception}/ResponseException.php (91%) create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Exception/ServerErrorException.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/HttpClient.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/MiddlewareHandler.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/MiddlewareInterface.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/ProcedureHandler.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Request/BatchRequestParser.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Request/RequestBuilder.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Request/RequestParser.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Response/ResponseBuilder.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Response/ResponseParser.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Validator/HostValidator.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Validator/JsonEncodingValidator.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Validator/JsonFormatValidator.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Validator/RpcFormatValidator.php create mode 100644 sources/vendor/fguillot/json-rpc/src/JsonRPC/Validator/UserValidator.php create mode 100644 sources/vendor/fguillot/picodb/lib/PicoDb/Builder/BaseBuilder.php rename sources/vendor/fguillot/picodb/lib/PicoDb/{Condition.php => Builder/ConditionBuilder.php} (90%) create mode 100644 sources/vendor/fguillot/picodb/lib/PicoDb/Builder/InsertBuilder.php create mode 100644 sources/vendor/fguillot/picodb/lib/PicoDb/Builder/UpdateBuilder.php create mode 100644 sources/vendor/fguillot/picodb/lib/PicoDb/DriverFactory.php create mode 100644 sources/vendor/fguillot/picodb/lib/PicoDb/LargeObject.php create mode 100644 sources/vendor/fguillot/picodb/lib/PicoDb/StatementHandler.php create mode 100644 sources/vendor/fguillot/picodb/lib/PicoDb/UrlParser.php create mode 100644 sources/vendor/fguillot/simple-queue/LICENSE create mode 100644 sources/vendor/fguillot/simple-queue/src/Adapter/AmqpQueueAdapter.php create mode 100644 sources/vendor/fguillot/simple-queue/src/Adapter/AwsSqsQueueAdapter.php create mode 100644 sources/vendor/fguillot/simple-queue/src/Adapter/BeanstalkQueueAdapter.php create mode 100644 sources/vendor/fguillot/simple-queue/src/Adapter/DisqueQueueAdapter.php create mode 100644 sources/vendor/fguillot/simple-queue/src/Adapter/MemoryQueueAdapter.php create mode 100644 sources/vendor/fguillot/simple-queue/src/Exception/NotSupportedException.php create mode 100644 sources/vendor/fguillot/simple-queue/src/Job.php create mode 100644 sources/vendor/fguillot/simple-queue/src/Queue.php create mode 100644 sources/vendor/fguillot/simple-queue/src/QueueAdapterInterface.php create mode 100644 sources/vendor/fguillot/simple-validator/src/SimpleValidator/Validators/NotEmpty.php create mode 100644 sources/vendor/fguillot/simpleLogger/src/SimpleLogger/Stderr.php create mode 100644 sources/vendor/fguillot/simpleLogger/src/SimpleLogger/Stdout.php create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/command/command_10.php create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/command/command_11.php create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/command/command_8.php create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/command/command_9.php create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/output/output_10.txt create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/output/output_11.txt create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/output/output_8.txt create mode 100644 sources/vendor/symfony/console/Tests/Fixtures/Style/SymfonyStyle/output/output_9.txt create mode 100644 sources/vendor/symfony/console/Tests/Helper/HelperTest.php create mode 100644 sources/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php delete mode 100644 sources/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.ser create mode 100644 sources/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php delete mode 100644 sources/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.ser diff --git a/README.markdown b/README.markdown index 5b4e2f1..e1216e6 100644 --- a/README.markdown +++ b/README.markdown @@ -33,7 +33,7 @@ From command line: Infos ----- -Kanboard v1.0.27 +Kanboard v1.0.31 Yunohost forum thread: @@ -63,7 +63,7 @@ Test it Test or upgrade to dev version: ``` su - admin -git clone -b dev https://github.com/mbugeia/kanboard_ynh +git clone -b dev https://github.com/YunoHost-Apps/kanboard_ynh # to install sudo yunohost app install -l Kanboard /home/admin/kanboard_ynh # to upgrade diff --git a/conf/config.php b/conf/config.php index 272b14e..1bf41e4 100644 --- a/conf/config.php +++ b/conf/config.php @@ -3,8 +3,11 @@ // Enable/Disable debug define('DEBUG', false); -// Debug file path -define('DEBUG_FILE', __DIR__.DIRECTORY_SEPARATOR.'data'.DIRECTORY_SEPARATOR.'debug.log'); +// Available log drivers: syslog, stderr, stdout or file +define('LOG_DRIVER', ''); + +// Log filename if the log driver is "file" +define('LOG_FILE', __DIR__.DIRECTORY_SEPARATOR.'data'.DIRECTORY_SEPARATOR.'debug.log'); // Plugins directory define('PLUGINS_DIR', 'plugins'); @@ -46,6 +49,15 @@ define('DB_NAME', 'yuno_dbuser'); // Mysql/Postgres custom port (null = default port) define('DB_PORT', null); +// Mysql SSL key +define('DB_SSL_KEY', null); + +// Mysql SSL certificate +define('DB_SSL_CERT', null); + +// Mysql SSL CA +define('DB_SSL_CA', null); + // Enable LDAP authentication (false by default) define('LDAP_AUTH', false); @@ -101,6 +113,13 @@ define('LDAP_USER_ATTRIBUTE_EMAIL', 'mail'); // LDAP attribute to find groups in user profile define('LDAP_USER_ATTRIBUTE_GROUPS', 'memberof'); +// LDAP attribute for user avatar image: thumbnailPhoto or jpegPhoto +define('LDAP_USER_ATTRIBUTE_PHOTO', ''); + +// LDAP attribute for user language, example: 'preferredlanguage' +// Put an empty string to disable language sync +define('LDAP_USER_ATTRIBUTE_LANGUAGE', ''); + // Allow automatic LDAP user creation define('LDAP_USER_CREATION', true); @@ -123,6 +142,11 @@ define('LDAP_GROUP_BASE_DN', ''); // Example for ActiveDirectory: (&(objectClass=group)(sAMAccountName=%s*)) define('LDAP_GROUP_FILTER', ''); +// LDAP user group filter +// If this filter is configured, Kanboard will search user groups in LDAP_GROUP_BASE_DN with this filter +// Example for OpenLDAP: (&(objectClass=posixGroup)(memberUid=%s)) +define('LDAP_GROUP_USER_FILTER', ''); + // LDAP attribute for the group name define('LDAP_GROUP_ATTRIBUTE_NAME', 'cn'); @@ -147,9 +171,6 @@ define('ENABLE_HSTS', false); // Enable or disable "X-Frame-Options: DENY" HTTP header define('ENABLE_XFRAME', true); -// Enable syslog logging -define('ENABLE_SYSLOG', true); - // Escape html inside markdown text define('MARKDOWN_ESCAPE_HTML', true); diff --git a/sources/.htaccess b/sources/.htaccess new file mode 100644 index 0000000..45123a9 --- /dev/null +++ b/sources/.htaccess @@ -0,0 +1,26 @@ + + Options -MultiViews + + SetEnv HTTP_MOD_REWRITE On + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + + + + + = 2.3> + Require all denied + + + Order allow,deny + Deny from all + + + + + Order allow,deny + Deny from all + + diff --git a/sources/ChangeLog b/sources/ChangeLog index f07ba9e..7e6662d 100644 --- a/sources/ChangeLog +++ b/sources/ChangeLog @@ -1,3 +1,135 @@ +Version 1.0.31 +-------------- + +New features: + +* Added tags: global and specific by project +* Added application and project roles validation for API procedure calls +* Added new API call: "getProjectByIdentifier" +* Added new API calls for external task links, project attachments and subtask time tracking + +Improvements: + +* Use PHP 7 for the Docker image +* Preserve role for existing users when using ReverseProxy authentication +* Handle priority for task and project duplication +* Expose task reference field to the user interface +* Improve ICal export +* Added argument owner_id and identifier to project API calls +* Rewrite integration tests to run with Docker containers +* Use the same task form layout everywhere +* Removed some tasks dropdown menus that are now available with task edit form +* Make embedded documentation readable in multiple languages (if a translation is available) +* Added acceptance tests (browser tests) + +Bug fixes: + +* Fixed broken CSV exports +* Fixed identical background color for LetterAvatar on 32bits platforms (Hash greater than PHP_MAX_INT) +* Fixed lexer issue with non word characters +* Flush memory cache in worker to get latest config values +* Fixed empty title for web notification with only one overdue task +* Take default swimlane into consideration for SwimlaneModel::getFirstActiveSwimlane() +* Fixed "due today" highlighting + +Version 1.0.30 +-------------- + +Improvements: + +* Show tasks that are due today in a different color + +Bug fixes: + +* Fixed wrong controller for search in dashboard +* Fixed plural form in alert message +* Fixed CSS cosmetic issue with popover and tooltips + +Version 1.0.29 +-------------- + +New features: + +* Manage plugin from the user interface and from the command line +* Added support for background workers +* Added the possibility to convert a subtask to a task +* Added menu entry to add tasks from all project views +* Add tasks in bulk from the board +* Add dropdown for projects +* Added config parameter to allow self-signed certificates for the HTTP client + +Improvements: + +* Display local date format in date picker +* Configure email settings with the user interface in addition to config file +* Upgrade Docker image to Alpine Linux 3.4 +* Move task import to a separate section +* Mark web notification as read when clicking on it +* Support strtotime strings for date search +* Reset failed login counter and unlock user when changing password +* Task do not open anymore in a new window on the Gantt chart +* Do not display task progress for tasks with no start/end date +* Use Gulp and Bower to manage assets +* Controller and Middleware refactoring +* Replace jQuery mobile detection by the library isMobile + +Bug fixes: + +* Fixed user date format parsing for dates that can be valid in multiple formats +* Do not sync user role if LDAP groups are not configured +* Fixed issue with unicode handling for letter based avatars and user initials +* Do not send notifications to disabled users +* Fixed wrong redirect when removing a task from the task view page + +Breaking changes: + +* Webhook to create tasks have been removed, use the API instead +* All controllers have been renamed, people who are not using URL rewriting will see different URLs +* All models have been renamed, plugin maintainers will have to update their plugins + +Version 1.0.28 +-------------- + +New features: + +* Added automated action to change task color based on the priority +* Added support for LDAP Posix Groups (OpenLDAP with memberUid or groupOfNames) +* Added support for LDAP user photo attribute (Avatar image) +* Added support for language LDAP attribute +* Added support for Mysql SSL connection +* Search in activity stream +* Search in comments +* Search by task creator +* Added command line utility to reset user password and to disable 2FA + +Improvements: + +* Improve Avatar upload form +* User roles are now synced with LDAP at each login +* Improve web page title on the task view +* Unify task drop-down menu between different views +* Improve LDAP user group membership synchronization +* Category and user filters do not append anymore in search field +* Added more template hooks +* Added tasks search with the API +* Added priority field to API procedures +* Added API procedure "getMemberGroups" +* Added parameters for overdue tasks notifications: group by projects and send only to managers +* Allow people to install Kanboard outside of the DocumentRoot +* Allow plugins to be loaded from another folder +* Filter/Lexer/QueryBuilder refactoring + +Bug fixes: + +* Allow a project owner to manage his own public project +* Fixed PHP warning when removing a user with no Avatar image +* Fixed improper Markdown escaping for some tooltips +* Closing all tasks by column, also update closed tasks +* Fixed wrong task link generation within Markdown text +* Fixed wrong URL on comment toggle link for sorting +* Fixed form submission with Meta+Enter keyboard shortcut +* Removed PHP notices in comment suppression view + Version 1.0.27 -------------- diff --git a/sources/app/Action/CommentCreation.php b/sources/app/Action/CommentCreation.php index b91e39e..60ca24f 100644 --- a/sources/app/Action/CommentCreation.php +++ b/sources/app/Action/CommentCreation.php @@ -65,11 +65,11 @@ class CommentCreation extends Base */ public function doAction(array $data) { - return (bool) $this->comment->create(array( + return (bool) $this->commentModel->create(array( 'reference' => isset($data['reference']) ? $data['reference'] : '', 'comment' => $data['comment'], 'task_id' => $data['task_id'], - 'user_id' => isset($data['user_id']) && $this->projectPermission->isAssignable($this->getProjectId(), $data['user_id']) ? $data['user_id'] : 0, + 'user_id' => isset($data['user_id']) && $this->projectPermissionModel->isAssignable($this->getProjectId(), $data['user_id']) ? $data['user_id'] : 0, )); } diff --git a/sources/app/Action/CommentCreationMoveTaskColumn.php b/sources/app/Action/CommentCreationMoveTaskColumn.php index 11224d6..1b16f48 100644 --- a/sources/app/Action/CommentCreationMoveTaskColumn.php +++ b/sources/app/Action/CommentCreationMoveTaskColumn.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Add a comment of the triggering event to the task description. @@ -32,7 +32,7 @@ class CommentCreationMoveTaskColumn extends Base public function getCompatibleEvents() { return array( - Task::EVENT_MOVE_COLUMN, + TaskModel::EVENT_MOVE_COLUMN, ); } @@ -71,9 +71,9 @@ class CommentCreationMoveTaskColumn extends Base return false; } - $column = $this->column->getById($data['column_id']); + $column = $this->columnModel->getById($data['column_id']); - return (bool) $this->comment->create(array( + return (bool) $this->commentModel->create(array( 'comment' => t('Moved to column %s', $column['title']), 'task_id' => $data['task_id'], 'user_id' => $this->userSession->getId(), diff --git a/sources/app/Action/TaskAssignCategoryColor.php b/sources/app/Action/TaskAssignCategoryColor.php index f5085cb..fc48687 100644 --- a/sources/app/Action/TaskAssignCategoryColor.php +++ b/sources/app/Action/TaskAssignCategoryColor.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Set a category automatically according to the color @@ -32,7 +32,7 @@ class TaskAssignCategoryColor extends Base public function getCompatibleEvents() { return array( - Task::EVENT_CREATE_UPDATE, + TaskModel::EVENT_CREATE_UPDATE, ); } @@ -78,7 +78,7 @@ class TaskAssignCategoryColor extends Base 'category_id' => $this->getParam('category_id'), ); - return $this->taskModification->update($values); + return $this->taskModificationModel->update($values); } /** diff --git a/sources/app/Action/TaskAssignCategoryLabel.php b/sources/app/Action/TaskAssignCategoryLabel.php index 95fa116..4829901 100644 --- a/sources/app/Action/TaskAssignCategoryLabel.php +++ b/sources/app/Action/TaskAssignCategoryLabel.php @@ -74,7 +74,7 @@ class TaskAssignCategoryLabel extends Base 'category_id' => $this->getParam('category_id'), ); - return $this->taskModification->update($values); + return $this->taskModificationModel->update($values); } /** diff --git a/sources/app/Action/TaskAssignCategoryLink.php b/sources/app/Action/TaskAssignCategoryLink.php index b39e41b..6937edd 100644 --- a/sources/app/Action/TaskAssignCategoryLink.php +++ b/sources/app/Action/TaskAssignCategoryLink.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\TaskLink; +use Kanboard\Model\TaskLinkModel; /** * Set a category automatically according to a task link @@ -33,7 +33,7 @@ class TaskAssignCategoryLink extends Base public function getCompatibleEvents() { return array( - TaskLink::EVENT_CREATE_UPDATE, + TaskLinkModel::EVENT_CREATE_UPDATE, ); } @@ -79,7 +79,7 @@ class TaskAssignCategoryLink extends Base 'category_id' => $this->getParam('category_id'), ); - return $this->taskModification->update($values); + return $this->taskModificationModel->update($values); } /** @@ -92,7 +92,7 @@ class TaskAssignCategoryLink extends Base public function hasRequiredCondition(array $data) { if ($data['link_id'] == $this->getParam('link_id')) { - $task = $this->taskFinder->getById($data['task_id']); + $task = $this->taskFinderModel->getById($data['task_id']); return empty($task['category_id']); } diff --git a/sources/app/Action/TaskAssignColorCategory.php b/sources/app/Action/TaskAssignColorCategory.php index 139c24c..284b8f4 100644 --- a/sources/app/Action/TaskAssignColorCategory.php +++ b/sources/app/Action/TaskAssignColorCategory.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Assign a color to a specific category @@ -32,7 +32,7 @@ class TaskAssignColorCategory extends Base public function getCompatibleEvents() { return array( - Task::EVENT_CREATE_UPDATE, + TaskModel::EVENT_CREATE_UPDATE, ); } @@ -78,7 +78,7 @@ class TaskAssignColorCategory extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModificationModel->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorColumn.php b/sources/app/Action/TaskAssignColorColumn.php index 9241273..57fd6f4 100644 --- a/sources/app/Action/TaskAssignColorColumn.php +++ b/sources/app/Action/TaskAssignColorColumn.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Assign a color to a task @@ -32,8 +32,8 @@ class TaskAssignColorColumn extends Base public function getCompatibleEvents() { return array( - Task::EVENT_CREATE, - Task::EVENT_MOVE_COLUMN, + TaskModel::EVENT_CREATE, + TaskModel::EVENT_MOVE_COLUMN, ); } @@ -79,7 +79,7 @@ class TaskAssignColorColumn extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModificationModel->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorLink.php b/sources/app/Action/TaskAssignColorLink.php index 12ceabb..9ab5458 100644 --- a/sources/app/Action/TaskAssignColorLink.php +++ b/sources/app/Action/TaskAssignColorLink.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\TaskLink; +use Kanboard\Model\TaskLinkModel; /** * Assign a color to a specific task link @@ -32,7 +32,7 @@ class TaskAssignColorLink extends Base public function getCompatibleEvents() { return array( - TaskLink::EVENT_CREATE_UPDATE, + TaskLinkModel::EVENT_CREATE_UPDATE, ); } @@ -78,7 +78,7 @@ class TaskAssignColorLink extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModificationModel->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignColorPriority.php b/sources/app/Action/TaskAssignColorPriority.php new file mode 100644 index 0000000..eae1b77 --- /dev/null +++ b/sources/app/Action/TaskAssignColorPriority.php @@ -0,0 +1,95 @@ + t('Color'), + 'priority' => t('Priority'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'priority', + ); + } + + /** + * Execute the action (change 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->taskModificationModel->update($values, 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['priority'] == $this->getParam('priority'); + } +} diff --git a/sources/app/Action/TaskAssignColorUser.php b/sources/app/Action/TaskAssignColorUser.php index 6ec8ce9..4bcf7a5 100644 --- a/sources/app/Action/TaskAssignColorUser.php +++ b/sources/app/Action/TaskAssignColorUser.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Assign a color to a specific user @@ -32,8 +32,8 @@ class TaskAssignColorUser extends Base public function getCompatibleEvents() { return array( - Task::EVENT_CREATE, - Task::EVENT_ASSIGNEE_CHANGE, + TaskModel::EVENT_CREATE, + TaskModel::EVENT_ASSIGNEE_CHANGE, ); } @@ -79,7 +79,7 @@ class TaskAssignColorUser extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->taskModification->update($values, false); + return $this->taskModificationModel->update($values, false); } /** diff --git a/sources/app/Action/TaskAssignCurrentUser.php b/sources/app/Action/TaskAssignCurrentUser.php index 192a120..997aa98 100644 --- a/sources/app/Action/TaskAssignCurrentUser.php +++ b/sources/app/Action/TaskAssignCurrentUser.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Assign a task to the logged user @@ -32,7 +32,7 @@ class TaskAssignCurrentUser extends Base public function getCompatibleEvents() { return array( - Task::EVENT_CREATE, + TaskModel::EVENT_CREATE, ); } @@ -78,7 +78,7 @@ class TaskAssignCurrentUser extends Base 'owner_id' => $this->userSession->getId(), ); - return $this->taskModification->update($values); + return $this->taskModificationModel->update($values); } /** diff --git a/sources/app/Action/TaskAssignCurrentUserColumn.php b/sources/app/Action/TaskAssignCurrentUserColumn.php index 05d08dd..bc28a90 100644 --- a/sources/app/Action/TaskAssignCurrentUserColumn.php +++ b/sources/app/Action/TaskAssignCurrentUserColumn.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Assign a task to the logged user on column change @@ -32,7 +32,7 @@ class TaskAssignCurrentUserColumn extends Base public function getCompatibleEvents() { return array( - Task::EVENT_MOVE_COLUMN, + TaskModel::EVENT_MOVE_COLUMN, ); } @@ -81,7 +81,7 @@ class TaskAssignCurrentUserColumn extends Base 'owner_id' => $this->userSession->getId(), ); - return $this->taskModification->update($values); + return $this->taskModificationModel->update($values); } /** diff --git a/sources/app/Action/TaskAssignSpecificUser.php b/sources/app/Action/TaskAssignSpecificUser.php index 2dc3e96..50a2b2a 100644 --- a/sources/app/Action/TaskAssignSpecificUser.php +++ b/sources/app/Action/TaskAssignSpecificUser.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Assign a task to a specific user @@ -32,8 +32,8 @@ class TaskAssignSpecificUser extends Base public function getCompatibleEvents() { return array( - Task::EVENT_CREATE_UPDATE, - Task::EVENT_MOVE_COLUMN, + TaskModel::EVENT_CREATE_UPDATE, + TaskModel::EVENT_MOVE_COLUMN, ); } @@ -79,7 +79,7 @@ class TaskAssignSpecificUser extends Base 'owner_id' => $this->getParam('user_id'), ); - return $this->taskModification->update($values); + return $this->taskModificationModel->update($values); } /** diff --git a/sources/app/Action/TaskAssignUser.php b/sources/app/Action/TaskAssignUser.php index da54d18..9ea2298 100644 --- a/sources/app/Action/TaskAssignUser.php +++ b/sources/app/Action/TaskAssignUser.php @@ -71,7 +71,7 @@ class TaskAssignUser extends Base 'owner_id' => $data['owner_id'], ); - return $this->taskModification->update($values); + return $this->taskModificationModel->update($values); } /** @@ -83,6 +83,6 @@ class TaskAssignUser extends Base */ public function hasRequiredCondition(array $data) { - return $this->projectPermission->isAssignable($this->getProjectId(), $data['owner_id']); + return $this->projectPermissionModel->isAssignable($this->getProjectId(), $data['owner_id']); } } diff --git a/sources/app/Action/TaskClose.php b/sources/app/Action/TaskClose.php index cf91e83..91e8cf4 100644 --- a/sources/app/Action/TaskClose.php +++ b/sources/app/Action/TaskClose.php @@ -63,7 +63,7 @@ class TaskClose extends Base */ public function doAction(array $data) { - return $this->taskStatus->close($data['task_id']); + return $this->taskStatusModel->close($data['task_id']); } /** diff --git a/sources/app/Action/TaskCloseColumn.php b/sources/app/Action/TaskCloseColumn.php index 09af3b9..1edce8f 100644 --- a/sources/app/Action/TaskCloseColumn.php +++ b/sources/app/Action/TaskCloseColumn.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Close automatically a task in a specific column @@ -32,7 +32,7 @@ class TaskCloseColumn extends Base public function getCompatibleEvents() { return array( - Task::EVENT_MOVE_COLUMN, + TaskModel::EVENT_MOVE_COLUMN, ); } @@ -67,7 +67,7 @@ class TaskCloseColumn extends Base */ public function doAction(array $data) { - return $this->taskStatus->close($data['task_id']); + return $this->taskStatusModel->close($data['task_id']); } /** diff --git a/sources/app/Action/TaskCloseNoActivity.php b/sources/app/Action/TaskCloseNoActivity.php index 59f7f56..5a10510 100644 --- a/sources/app/Action/TaskCloseNoActivity.php +++ b/sources/app/Action/TaskCloseNoActivity.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Close automatically a task after when inactive @@ -31,7 +31,7 @@ class TaskCloseNoActivity extends Base */ public function getCompatibleEvents() { - return array(Task::EVENT_DAILY_CRONJOB); + return array(TaskModel::EVENT_DAILY_CRONJOB); } /** @@ -74,7 +74,7 @@ class TaskCloseNoActivity extends Base $duration = time() - $task['date_modification']; if ($duration > $max) { - $results[] = $this->taskStatus->close($task['id']); + $results[] = $this->taskStatusModel->close($task['id']); } } diff --git a/sources/app/Action/TaskCreation.php b/sources/app/Action/TaskCreation.php index 290c31e..e9e5c5f 100644 --- a/sources/app/Action/TaskCreation.php +++ b/sources/app/Action/TaskCreation.php @@ -66,7 +66,7 @@ class TaskCreation extends Base */ public function doAction(array $data) { - return (bool) $this->taskCreation->create(array( + return (bool) $this->taskCreationModel->create(array( 'project_id' => $data['project_id'], 'title' => $data['title'], 'reference' => $data['reference'], diff --git a/sources/app/Action/TaskDuplicateAnotherProject.php b/sources/app/Action/TaskDuplicateAnotherProject.php index 5f05136..d70d2ee 100644 --- a/sources/app/Action/TaskDuplicateAnotherProject.php +++ b/sources/app/Action/TaskDuplicateAnotherProject.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Duplicate a task to another project @@ -32,8 +32,9 @@ class TaskDuplicateAnotherProject extends Base public function getCompatibleEvents() { return array( - Task::EVENT_MOVE_COLUMN, - Task::EVENT_CLOSE, + TaskModel::EVENT_MOVE_COLUMN, + TaskModel::EVENT_CLOSE, + TaskModel::EVENT_CREATE, ); } @@ -74,8 +75,8 @@ class TaskDuplicateAnotherProject extends Base */ public function doAction(array $data) { - $destination_column_id = $this->column->getFirstColumnId($this->getParam('project_id')); - return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); + $destination_column_id = $this->columnModel->getFirstColumnId($this->getParam('project_id')); + return (bool) $this->taskProjectDuplicationModel->duplicateToProject($data['task_id'], $this->getParam('project_id'), null, $destination_column_id); } /** diff --git a/sources/app/Action/TaskEmail.php b/sources/app/Action/TaskEmail.php index 4e0e06a..7f9ba41 100644 --- a/sources/app/Action/TaskEmail.php +++ b/sources/app/Action/TaskEmail.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Email a task to someone @@ -32,8 +32,8 @@ class TaskEmail extends Base public function getCompatibleEvents() { return array( - Task::EVENT_MOVE_COLUMN, - Task::EVENT_CLOSE, + TaskModel::EVENT_MOVE_COLUMN, + TaskModel::EVENT_CLOSE, ); } @@ -75,16 +75,16 @@ class TaskEmail extends Base */ public function doAction(array $data) { - $user = $this->user->getById($this->getParam('user_id')); + $user = $this->userModel->getById($this->getParam('user_id')); if (! empty($user['email'])) { - $task = $this->taskFinder->getDetails($data['task_id']); + $task = $this->taskFinderModel->getDetails($data['task_id']); $this->emailClient->send( $user['email'], $user['name'] ?: $user['username'], $this->getParam('subject'), - $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url'))) + $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->configModel->get('application_url'))) ); return true; diff --git a/sources/app/Action/TaskEmailNoActivity.php b/sources/app/Action/TaskEmailNoActivity.php index c5d7a79..c60702f 100644 --- a/sources/app/Action/TaskEmailNoActivity.php +++ b/sources/app/Action/TaskEmailNoActivity.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Email a task with no activity @@ -32,7 +32,7 @@ class TaskEmailNoActivity extends Base public function getCompatibleEvents() { return array( - Task::EVENT_DAILY_CRONJOB, + TaskModel::EVENT_DAILY_CRONJOB, ); } @@ -85,7 +85,7 @@ class TaskEmailNoActivity extends Base { $results = array(); $max = $this->getParam('duration') * 86400; - $user = $this->user->getById($this->getParam('user_id')); + $user = $this->userModel->getById($this->getParam('user_id')); if (! empty($user['email'])) { foreach ($data['tasks'] as $task) { @@ -110,13 +110,13 @@ class TaskEmailNoActivity extends Base */ private function sendEmail($task_id, array $user) { - $task = $this->taskFinder->getDetails($task_id); + $task = $this->taskFinderModel->getDetails($task_id); $this->emailClient->send( $user['email'], $user['name'] ?: $user['username'], $this->getParam('subject'), - $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url'))) + $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->configModel->get('application_url'))) ); return true; diff --git a/sources/app/Action/TaskMoveAnotherProject.php b/sources/app/Action/TaskMoveAnotherProject.php index fdff0d8..66635a6 100644 --- a/sources/app/Action/TaskMoveAnotherProject.php +++ b/sources/app/Action/TaskMoveAnotherProject.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Move a task to another project @@ -32,8 +32,8 @@ class TaskMoveAnotherProject extends Base public function getCompatibleEvents() { return array( - Task::EVENT_MOVE_COLUMN, - Task::EVENT_CLOSE, + TaskModel::EVENT_MOVE_COLUMN, + TaskModel::EVENT_CLOSE, ); } @@ -75,7 +75,7 @@ class TaskMoveAnotherProject extends Base */ public function doAction(array $data) { - return $this->taskDuplication->moveToProject($data['task_id'], $this->getParam('project_id')); + return $this->taskProjectMoveModel->moveToProject($data['task_id'], $this->getParam('project_id')); } /** diff --git a/sources/app/Action/TaskMoveColumnAssigned.php b/sources/app/Action/TaskMoveColumnAssigned.php index 1b23a59..7e3db9c 100644 --- a/sources/app/Action/TaskMoveColumnAssigned.php +++ b/sources/app/Action/TaskMoveColumnAssigned.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Move a task to another column when an assignee is set @@ -32,8 +32,8 @@ class TaskMoveColumnAssigned extends Base public function getCompatibleEvents() { return array( - Task::EVENT_ASSIGNEE_CHANGE, - Task::EVENT_UPDATE, + TaskModel::EVENT_ASSIGNEE_CHANGE, + TaskModel::EVENT_UPDATE, ); } @@ -75,9 +75,9 @@ class TaskMoveColumnAssigned extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinder->getById($data['task_id']); + $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPosition->movePosition( + return $this->taskPositionModel->movePosition( $data['project_id'], $data['task_id'], $this->getParam('dest_column_id'), diff --git a/sources/app/Action/TaskMoveColumnCategoryChange.php b/sources/app/Action/TaskMoveColumnCategoryChange.php index 0f591ed..e4f8876 100644 --- a/sources/app/Action/TaskMoveColumnCategoryChange.php +++ b/sources/app/Action/TaskMoveColumnCategoryChange.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Move a task to another column when the category is changed @@ -32,7 +32,7 @@ class TaskMoveColumnCategoryChange extends Base public function getCompatibleEvents() { return array( - Task::EVENT_UPDATE, + TaskModel::EVENT_UPDATE, ); } @@ -74,9 +74,9 @@ class TaskMoveColumnCategoryChange extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinder->getById($data['task_id']); + $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPosition->movePosition( + return $this->taskPositionModel->movePosition( $data['project_id'], $data['task_id'], $this->getParam('dest_column_id'), diff --git a/sources/app/Action/TaskMoveColumnUnAssigned.php b/sources/app/Action/TaskMoveColumnUnAssigned.php index 99ef935..c3ae9e1 100644 --- a/sources/app/Action/TaskMoveColumnUnAssigned.php +++ b/sources/app/Action/TaskMoveColumnUnAssigned.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Move a task to another column when an assignee is cleared @@ -32,8 +32,8 @@ class TaskMoveColumnUnAssigned extends Base public function getCompatibleEvents() { return array( - Task::EVENT_ASSIGNEE_CHANGE, - Task::EVENT_UPDATE, + TaskModel::EVENT_ASSIGNEE_CHANGE, + TaskModel::EVENT_UPDATE, ); } @@ -75,9 +75,9 @@ class TaskMoveColumnUnAssigned extends Base */ public function doAction(array $data) { - $original_task = $this->taskFinder->getById($data['task_id']); + $original_task = $this->taskFinderModel->getById($data['task_id']); - return $this->taskPosition->movePosition( + return $this->taskPositionModel->movePosition( $data['project_id'], $data['task_id'], $this->getParam('dest_column_id'), diff --git a/sources/app/Action/TaskOpen.php b/sources/app/Action/TaskOpen.php index ec0f96f..8e847b8 100644 --- a/sources/app/Action/TaskOpen.php +++ b/sources/app/Action/TaskOpen.php @@ -63,7 +63,7 @@ class TaskOpen extends Base */ public function doAction(array $data) { - return $this->taskStatus->open($data['task_id']); + return $this->taskStatusModel->open($data['task_id']); } /** diff --git a/sources/app/Action/TaskUpdateStartDate.php b/sources/app/Action/TaskUpdateStartDate.php index e5cea01..e5410a8 100644 --- a/sources/app/Action/TaskUpdateStartDate.php +++ b/sources/app/Action/TaskUpdateStartDate.php @@ -2,7 +2,7 @@ namespace Kanboard\Action; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Set the start date of task @@ -32,7 +32,7 @@ class TaskUpdateStartDate extends Base public function getCompatibleEvents() { return array( - Task::EVENT_MOVE_COLUMN, + TaskModel::EVENT_MOVE_COLUMN, ); } @@ -77,7 +77,7 @@ class TaskUpdateStartDate extends Base 'date_started' => time(), ); - return $this->taskModification->update($values, false); + return $this->taskModificationModel->update($values, false); } /** diff --git a/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php b/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php index 62c8355..d6cd1f8 100644 --- a/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php +++ b/sources/app/Analytic/AverageLeadCycleTimeAnalytic.php @@ -3,7 +3,7 @@ namespace Kanboard\Analytic; use Kanboard\Core\Base; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Average Lead and Cycle Time @@ -106,7 +106,7 @@ class AverageLeadCycleTimeAnalytic extends Base private function getTasks($project_id) { return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->columns('date_completed', 'date_creation', 'date_started') ->eq('project_id', $project_id) ->desc('id') diff --git a/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php b/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php index 1107832..3556fb9 100644 --- a/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php +++ b/sources/app/Analytic/AverageTimeSpentColumnAnalytic.php @@ -3,7 +3,7 @@ namespace Kanboard\Analytic; use Kanboard\Core\Base; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Average Time Spent by Column @@ -40,7 +40,7 @@ class AverageTimeSpentColumnAnalytic extends Base private function initialize($project_id) { $stats = array(); - $columns = $this->column->getList($project_id); + $columns = $this->columnModel->getList($project_id); foreach ($columns as $column_id => $column_title) { $stats[$column_id] = array( @@ -110,7 +110,7 @@ class AverageTimeSpentColumnAnalytic extends Base */ private function getTaskTimeByColumns(array &$task) { - $columns = $this->transition->getTimeSpentByTask($task['id']); + $columns = $this->transitionModel->getTimeSpentByTask($task['id']); if (! isset($columns[$task['column_id']])) { $columns[$task['column_id']] = 0; @@ -144,7 +144,7 @@ class AverageTimeSpentColumnAnalytic extends Base private function getTasks($project_id) { return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->columns('id', 'date_completed', 'date_moved', 'column_id') ->eq('project_id', $project_id) ->desc('id') diff --git a/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php b/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php index 490bcd5..d9aea32 100644 --- a/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php +++ b/sources/app/Analytic/EstimatedTimeComparisonAnalytic.php @@ -3,7 +3,7 @@ namespace Kanboard\Analytic; use Kanboard\Core\Base; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; /** * Estimated/Spent Time Comparison @@ -22,7 +22,7 @@ class EstimatedTimeComparisonAnalytic extends Base */ public function build($project_id) { - $rows = $this->db->table(Task::TABLE) + $rows = $this->db->table(TaskModel::TABLE) ->columns('SUM(time_estimated) AS time_estimated', 'SUM(time_spent) AS time_spent', 'is_active') ->eq('project_id', $project_id) ->groupBy('is_active') @@ -40,7 +40,7 @@ class EstimatedTimeComparisonAnalytic extends Base ); foreach ($rows as $row) { - $key = $row['is_active'] == Task::STATUS_OPEN ? 'open' : 'closed'; + $key = $row['is_active'] == TaskModel::STATUS_OPEN ? 'open' : 'closed'; $metrics[$key]['time_spent'] = $row['time_spent']; $metrics[$key]['time_estimated'] = $row['time_estimated']; } diff --git a/sources/app/Analytic/TaskDistributionAnalytic.php b/sources/app/Analytic/TaskDistributionAnalytic.php index 838652e..447d88a 100644 --- a/sources/app/Analytic/TaskDistributionAnalytic.php +++ b/sources/app/Analytic/TaskDistributionAnalytic.php @@ -23,10 +23,10 @@ class TaskDistributionAnalytic extends Base { $metrics = array(); $total = 0; - $columns = $this->column->getAll($project_id); + $columns = $this->columnModel->getAll($project_id); foreach ($columns as $column) { - $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']); + $nb_tasks = $this->taskFinderModel->countByColumnId($project_id, $column['id']); $total += $nb_tasks; $metrics[] = array( diff --git a/sources/app/Analytic/UserDistributionAnalytic.php b/sources/app/Analytic/UserDistributionAnalytic.php index e1815f9..421d424 100644 --- a/sources/app/Analytic/UserDistributionAnalytic.php +++ b/sources/app/Analytic/UserDistributionAnalytic.php @@ -23,8 +23,8 @@ class UserDistributionAnalytic extends Base { $metrics = array(); $total = 0; - $tasks = $this->taskFinder->getAll($project_id); - $users = $this->projectUserRole->getAssignableUsersList($project_id); + $tasks = $this->taskFinderModel->getAll($project_id); + $users = $this->projectUserRoleModel->getAssignableUsersList($project_id); foreach ($tasks as $task) { $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0]; diff --git a/sources/app/Api/Authorization/ActionAuthorization.php b/sources/app/Api/Authorization/ActionAuthorization.php new file mode 100644 index 0000000..4b41ad8 --- /dev/null +++ b/sources/app/Api/Authorization/ActionAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->actionModel->getProjectId($action_id)); + } + } +} diff --git a/sources/app/Api/Authorization/CategoryAuthorization.php b/sources/app/Api/Authorization/CategoryAuthorization.php new file mode 100644 index 0000000..f17265a --- /dev/null +++ b/sources/app/Api/Authorization/CategoryAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->categoryModel->getProjectId($category_id)); + } + } +} diff --git a/sources/app/Api/Authorization/ColumnAuthorization.php b/sources/app/Api/Authorization/ColumnAuthorization.php new file mode 100644 index 0000000..37aecda --- /dev/null +++ b/sources/app/Api/Authorization/ColumnAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->columnModel->getProjectId($column_id)); + } + } +} diff --git a/sources/app/Api/Authorization/CommentAuthorization.php b/sources/app/Api/Authorization/CommentAuthorization.php new file mode 100644 index 0000000..ed15512 --- /dev/null +++ b/sources/app/Api/Authorization/CommentAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->commentModel->getProjectId($comment_id)); + } + } +} diff --git a/sources/app/Api/Authorization/ProcedureAuthorization.php b/sources/app/Api/Authorization/ProcedureAuthorization.php new file mode 100644 index 0000000..070a637 --- /dev/null +++ b/sources/app/Api/Authorization/ProcedureAuthorization.php @@ -0,0 +1,32 @@ +userSession->isLogged() && in_array($procedure, $this->userSpecificProcedures)) { + throw new AccessDeniedException('This procedure is not available with the API credentials'); + } + } +} diff --git a/sources/app/Api/Authorization/ProjectAuthorization.php b/sources/app/Api/Authorization/ProjectAuthorization.php new file mode 100644 index 0000000..21ecf31 --- /dev/null +++ b/sources/app/Api/Authorization/ProjectAuthorization.php @@ -0,0 +1,35 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $project_id); + } + } + + protected function checkProjectPermission($class, $method, $project_id) + { + if (empty($project_id)) { + throw new AccessDeniedException('Project not found'); + } + + $role = $this->projectUserRoleModel->getUserRole($project_id, $this->userSession->getId()); + + if (! $this->apiProjectAuthorization->isAllowed($class, $method, $role)) { + throw new AccessDeniedException('Project access denied'); + } + } +} diff --git a/sources/app/Api/Authorization/SubtaskAuthorization.php b/sources/app/Api/Authorization/SubtaskAuthorization.php new file mode 100644 index 0000000..fcb5792 --- /dev/null +++ b/sources/app/Api/Authorization/SubtaskAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->subtaskModel->getProjectId($subtask_id)); + } + } +} diff --git a/sources/app/Api/Authorization/TaskAuthorization.php b/sources/app/Api/Authorization/TaskAuthorization.php new file mode 100644 index 0000000..db93b76 --- /dev/null +++ b/sources/app/Api/Authorization/TaskAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->taskFinderModel->getProjectId($category_id)); + } + } +} diff --git a/sources/app/Api/Authorization/TaskFileAuthorization.php b/sources/app/Api/Authorization/TaskFileAuthorization.php new file mode 100644 index 0000000..e40783e --- /dev/null +++ b/sources/app/Api/Authorization/TaskFileAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->taskFileModel->getProjectId($file_id)); + } + } +} diff --git a/sources/app/Api/Authorization/TaskLinkAuthorization.php b/sources/app/Api/Authorization/TaskLinkAuthorization.php new file mode 100644 index 0000000..2f5fc8d --- /dev/null +++ b/sources/app/Api/Authorization/TaskLinkAuthorization.php @@ -0,0 +1,19 @@ +userSession->isLogged()) { + $this->checkProjectPermission($class, $method, $this->taskLinkModel->getProjectId($task_link_id)); + } + } +} diff --git a/sources/app/Api/Authorization/UserAuthorization.php b/sources/app/Api/Authorization/UserAuthorization.php new file mode 100644 index 0000000..3fd6865 --- /dev/null +++ b/sources/app/Api/Authorization/UserAuthorization.php @@ -0,0 +1,22 @@ +userSession->isLogged() && ! $this->apiAuthorization->isAllowed($class, $method, $this->userSession->getRole())) { + throw new AccessDeniedException('You are not allowed to access to this resource'); + } + } +} diff --git a/sources/app/Api/Base.php b/sources/app/Api/Base.php deleted file mode 100644 index 0959817..0000000 --- a/sources/app/Api/Base.php +++ /dev/null @@ -1,117 +0,0 @@ -both_allowed_procedures); - $is_user_procedure = in_array($procedure, $this->user_allowed_procedures); - - if ($is_user && ! $is_both_procedure && ! $is_user_procedure) { - throw new AccessDeniedException('Permission denied'); - } elseif (! $is_user && ! $is_both_procedure && $is_user_procedure) { - throw new AccessDeniedException('Permission denied'); - } - - $this->logger->debug('API call: '.$procedure); - } - - public function checkProjectPermission($project_id) - { - if ($this->userSession->isLogged() && ! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { - throw new AccessDeniedException('Permission denied'); - } - } - - public function checkTaskPermission($task_id) - { - if ($this->userSession->isLogged()) { - $this->checkProjectPermission($this->taskFinder->getProjectId($task_id)); - } - } - - protected function formatTask($task) - { - if (! empty($task)) { - $task['url'] = $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true); - $task['color'] = $this->color->getColorProperties($task['color_id']); - } - - return $task; - } - - protected function formatTasks($tasks) - { - if (! empty($tasks)) { - foreach ($tasks as &$task) { - $task = $this->formatTask($task); - } - } - - return $tasks; - } - - protected function formatProject($project) - { - if (! empty($project)) { - $project['url'] = array( - 'board' => $this->helper->url->to('board', 'show', array('project_id' => $project['id']), '', true), - 'calendar' => $this->helper->url->to('calendar', 'show', array('project_id' => $project['id']), '', true), - 'list' => $this->helper->url->to('listing', 'show', array('project_id' => $project['id']), '', true), - ); - } - - return $project; - } - - protected function formatProjects($projects) - { - if (! empty($projects)) { - foreach ($projects as &$project) { - $project = $this->formatProject($project); - } - } - - return $projects; - } -} diff --git a/sources/app/Api/Board.php b/sources/app/Api/Board.php deleted file mode 100644 index 185ac51..0000000 --- a/sources/app/Api/Board.php +++ /dev/null @@ -1,18 +0,0 @@ -checkProjectPermission($project_id); - return $this->board->getBoard($project_id); - } -} diff --git a/sources/app/Api/Category.php b/sources/app/Api/Category.php deleted file mode 100644 index fbd61c5..0000000 --- a/sources/app/Api/Category.php +++ /dev/null @@ -1,49 +0,0 @@ -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->categoryValidator->validateCreation($values); - return $valid ? $this->category->create($values) : false; - } - - public function updateCategory($id, $name) - { - $values = array( - 'id' => $id, - 'name' => $name, - ); - - list($valid, ) = $this->categoryValidator->validateModification($values); - return $valid && $this->category->update($values); - } -} diff --git a/sources/app/Api/Column.php b/sources/app/Api/Column.php deleted file mode 100644 index ddc3a5d..0000000 --- a/sources/app/Api/Column.php +++ /dev/null @@ -1,42 +0,0 @@ -column->getAll($project_id); - } - - public function getColumn($column_id) - { - return $this->column->getById($column_id); - } - - public function updateColumn($column_id, $title, $task_limit = 0, $description = '') - { - return $this->column->update($column_id, $title, $task_limit, $description); - } - - public function addColumn($project_id, $title, $task_limit = 0, $description = '') - { - return $this->column->create($project_id, $title, $task_limit, $description); - } - - public function removeColumn($column_id) - { - return $this->column->remove($column_id); - } - - public function changeColumnPosition($project_id, $column_id, $position) - { - return $this->column->changePosition($project_id, $column_id, $position); - } -} diff --git a/sources/app/Api/Comment.php b/sources/app/Api/Comment.php deleted file mode 100644 index 1fc1c70..0000000 --- a/sources/app/Api/Comment.php +++ /dev/null @@ -1,52 +0,0 @@ -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, $reference = '') - { - $values = array( - 'task_id' => $task_id, - 'user_id' => $user_id, - 'comment' => $content, - 'reference' => $reference, - ); - - list($valid, ) = $this->commentValidator->validateCreation($values); - - return $valid ? $this->comment->create($values) : false; - } - - public function updateComment($id, $content) - { - $values = array( - 'id' => $id, - 'comment' => $content, - ); - - list($valid, ) = $this->commentValidator->validateModification($values); - return $valid && $this->comment->update($values); - } -} diff --git a/sources/app/Api/File.php b/sources/app/Api/File.php deleted file mode 100644 index 71c31c7..0000000 --- a/sources/app/Api/File.php +++ /dev/null @@ -1,90 +0,0 @@ -taskFile->getById($file_id); - } - - public function getAllTaskFiles($task_id) - { - return $this->taskFile->getAll($task_id); - } - - public function downloadTaskFile($file_id) - { - try { - $file = $this->taskFile->getById($file_id); - - if (! empty($file)) { - return base64_encode($this->objectStorage->get($file['path'])); - } - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - return ''; - } - } - - public function createTaskFile($project_id, $task_id, $filename, $blob) - { - try { - return $this->taskFile->uploadContent($task_id, $filename, $blob); - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); - return false; - } - } - - public function removeTaskFile($file_id) - { - return $this->taskFile->remove($file_id); - } - - public function removeAllTaskFiles($task_id) - { - return $this->taskFile->removeAll($task_id); - } - - // Deprecated procedures - - public function getFile($file_id) - { - return $this->getTaskFile($file_id); - } - - public function getAllFiles($task_id) - { - return $this->getAllTaskFiles($task_id); - } - - public function downloadFile($file_id) - { - return $this->downloadTaskFile($file_id); - } - - public function createFile($project_id, $task_id, $filename, $blob) - { - return $this->createTaskFile($project_id, $task_id, $filename, $blob); - } - - public function removeFile($file_id) - { - return $this->removeTaskFile($file_id); - } - - public function removeAllFiles($task_id) - { - return $this->removeAllTaskFiles($task_id); - } -} diff --git a/sources/app/Api/GroupMember.php b/sources/app/Api/GroupMember.php deleted file mode 100644 index de62f0c..0000000 --- a/sources/app/Api/GroupMember.php +++ /dev/null @@ -1,32 +0,0 @@ -groupMember->getMembers($group_id); - } - - public function addGroupMember($group_id, $user_id) - { - return $this->groupMember->addUser($group_id, $user_id); - } - - public function removeGroupMember($group_id, $user_id) - { - return $this->groupMember->removeUser($group_id, $user_id); - } - - public function isGroupMember($group_id, $user_id) - { - return $this->groupMember->isMember($group_id, $user_id); - } -} diff --git a/sources/app/Api/Me.php b/sources/app/Api/Me.php deleted file mode 100644 index ccc809e..0000000 --- a/sources/app/Api/Me.php +++ /dev/null @@ -1,72 +0,0 @@ -sessionStorage->user; - } - - public function getMyDashboard() - { - $user_id = $this->userSession->getId(); - $projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id))->findAll(); - $tasks = $this->taskFinder->getUserQuery($user_id)->findAll(); - - return array( - 'projects' => $this->formatProjects($projects), - 'tasks' => $this->formatTasks($tasks), - 'subtasks' => $this->subtask->getUserQuery($user_id, array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))->findAll(), - ); - } - - public function getMyActivityStream() - { - $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); - return $this->projectActivity->getProjects($project_ids, 100); - } - - public function createMyPrivateProject($name, $description = null) - { - if ($this->config->get('disable_private_project', 0) == 1) { - return false; - } - - $values = array( - 'name' => $name, - 'description' => $description, - 'is_private' => 1, - ); - - list($valid, ) = $this->projectValidator->validateCreation($values); - return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false; - } - - public function getMyProjectsList() - { - return $this->projectUserRole->getProjectsByUser($this->userSession->getId()); - } - - public function getMyOverdueTasks() - { - return $this->taskFinder->getOverdueTasksByUser($this->userSession->getId()); - } - - public function getMyProjects() - { - $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); - $projects = $this->project->getAllByIds($project_ids); - - return $this->formatProjects($projects); - } -} diff --git a/sources/app/Api/Auth.php b/sources/app/Api/Middleware/AuthenticationMiddleware.php similarity index 51% rename from sources/app/Api/Auth.php rename to sources/app/Api/Middleware/AuthenticationMiddleware.php index 6c6e1eb..8e30959 100644 --- a/sources/app/Api/Auth.php +++ b/sources/app/Api/Middleware/AuthenticationMiddleware.php @@ -1,38 +1,39 @@ dispatcher->dispatch('app.bootstrap'); if ($this->isUserAuthenticated($username, $password)) { - $this->checkProcedurePermission(true, $method); - $this->userSession->initialize($this->user->getByUsername($username)); - } elseif ($this->isAppAuthenticated($username, $password)) { - $this->checkProcedurePermission(false, $method); - } else { + $this->userSession->initialize($this->userModel->getByUsername($username)); + } elseif (! $this->isAppAuthenticated($username, $password)) { $this->logger->error('API authentication failure for '.$username); - throw new AuthenticationFailure('Wrong credentials'); + throw new AuthenticationFailureException('Wrong credentials'); } } @@ -47,8 +48,8 @@ class Auth extends Base private function isUserAuthenticated($username, $password) { return $username !== 'jsonrpc' && - ! $this->userLocking->isLocked($username) && - $this->authenticationManager->passwordAuthentication($username, $password); + ! $this->userLockingModel->isLocked($username) && + $this->authenticationManager->passwordAuthentication($username, $password); } /** @@ -76,6 +77,6 @@ class Auth extends Base return API_AUTHENTICATION_TOKEN; } - return $this->config->get('api_token'); + return $this->configModel->get('api_token'); } } diff --git a/sources/app/Api/Action.php b/sources/app/Api/Procedure/ActionProcedure.php similarity index 70% rename from sources/app/Api/Action.php rename to sources/app/Api/Procedure/ActionProcedure.php index 9e3b86f..4043dbb 100644 --- a/sources/app/Api/Action.php +++ b/sources/app/Api/Procedure/ActionProcedure.php @@ -1,14 +1,17 @@ action->remove($action_id); + ActionAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeAction', $action_id); + return $this->actionModel->remove($action_id); } public function getActions($project_id) { - return $this->action->getAllByProject($project_id); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getActions', $project_id); + return $this->actionModel->getAllByProject($project_id); } public function createAction($project_id, $event_name, $action_name, array $params) { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createAction', $project_id); $values = array( 'project_id' => $project_id, 'event_name' => $event_name, @@ -80,6 +86,6 @@ class Action extends \Kanboard\Core\Base } } - return $this->action->create($values); + return $this->actionModel->create($values); } } diff --git a/sources/app/Api/App.php b/sources/app/Api/Procedure/AppProcedure.php similarity index 63% rename from sources/app/Api/App.php rename to sources/app/Api/Procedure/AppProcedure.php index 635f1ce..60af4a6 100644 --- a/sources/app/Api/App.php +++ b/sources/app/Api/Procedure/AppProcedure.php @@ -1,18 +1,18 @@ config->get('application_timezone'); + return $this->timezoneModel->getCurrentTimezone(); } public function getVersion() @@ -22,17 +22,17 @@ class App extends \Kanboard\Core\Base public function getDefaultTaskColor() { - return $this->color->getDefaultColor(); + return $this->colorModel->getDefaultColor(); } public function getDefaultTaskColors() { - return $this->color->getDefaultColors(); + return $this->colorModel->getDefaultColors(); } public function getColorList() { - return $this->color->getList(); + return $this->colorModel->getList(); } public function getApplicationRoles() diff --git a/sources/app/Api/Procedure/BaseProcedure.php b/sources/app/Api/Procedure/BaseProcedure.php new file mode 100644 index 0000000..e31b302 --- /dev/null +++ b/sources/app/Api/Procedure/BaseProcedure.php @@ -0,0 +1,85 @@ +container)->check($procedure); + UserAuthorization::getInstance($this->container)->check($this->getClassName(), $procedure); + } + + protected function formatTask($task) + { + if (! empty($task)) { + $task['url'] = $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true); + $task['color'] = $this->colorModel->getColorProperties($task['color_id']); + } + + return $task; + } + + protected function formatTasks($tasks) + { + if (! empty($tasks)) { + foreach ($tasks as &$task) { + $task = $this->formatTask($task); + } + } + + return $tasks; + } + + protected function formatProject($project) + { + if (! empty($project)) { + $project['url'] = array( + 'board' => $this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id']), '', true), + 'calendar' => $this->helper->url->to('CalendarController', 'show', array('project_id' => $project['id']), '', true), + 'list' => $this->helper->url->to('TaskListController', 'show', array('project_id' => $project['id']), '', true), + ); + } + + return $project; + } + + protected function formatProjects($projects) + { + if (! empty($projects)) { + foreach ($projects as &$project) { + $project = $this->formatProject($project); + } + } + + return $projects; + } + + protected function filterValues(array $values) + { + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + return $values; + } + + protected function getClassName() + { + $reflection = new ReflectionClass(get_called_class()); + return $reflection->getShortName(); + } +} diff --git a/sources/app/Api/Procedure/BoardProcedure.php b/sources/app/Api/Procedure/BoardProcedure.php new file mode 100644 index 0000000..674b546 --- /dev/null +++ b/sources/app/Api/Procedure/BoardProcedure.php @@ -0,0 +1,25 @@ +container)->check($this->getClassName(), 'getBoard', $project_id); + + return BoardFormatter::getInstance($this->container) + ->withProjectId($project_id) + ->withQuery($this->taskFinderModel->getExtendedQuery()) + ->format(); + } +} diff --git a/sources/app/Api/Procedure/CategoryProcedure.php b/sources/app/Api/Procedure/CategoryProcedure.php new file mode 100644 index 0000000..3ebbd90 --- /dev/null +++ b/sources/app/Api/Procedure/CategoryProcedure.php @@ -0,0 +1,59 @@ +container)->check($this->getClassName(), 'getCategory', $category_id); + return $this->categoryModel->getById($category_id); + } + + public function getAllCategories($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllCategories', $project_id); + return $this->categoryModel->getAll($project_id); + } + + public function removeCategory($category_id) + { + CategoryAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeCategory', $category_id); + return $this->categoryModel->remove($category_id); + } + + public function createCategory($project_id, $name) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createCategory', $project_id); + + $values = array( + 'project_id' => $project_id, + 'name' => $name, + ); + + list($valid, ) = $this->categoryValidator->validateCreation($values); + return $valid ? $this->categoryModel->create($values) : false; + } + + public function updateCategory($id, $name) + { + CategoryAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateCategory', $id); + + $values = array( + 'id' => $id, + 'name' => $name, + ); + + list($valid, ) = $this->categoryValidator->validateModification($values); + return $valid && $this->categoryModel->update($values); + } +} diff --git a/sources/app/Api/Procedure/ColumnProcedure.php b/sources/app/Api/Procedure/ColumnProcedure.php new file mode 100644 index 0000000..ab9d173 --- /dev/null +++ b/sources/app/Api/Procedure/ColumnProcedure.php @@ -0,0 +1,51 @@ +container)->check($this->getClassName(), 'getColumns', $project_id); + return $this->columnModel->getAll($project_id); + } + + public function getColumn($column_id) + { + ColumnAuthorization::getInstance($this->container)->check($this->getClassName(), 'getColumn', $column_id); + return $this->columnModel->getById($column_id); + } + + public function updateColumn($column_id, $title, $task_limit = 0, $description = '') + { + ColumnAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateColumn', $column_id); + return $this->columnModel->update($column_id, $title, $task_limit, $description); + } + + public function addColumn($project_id, $title, $task_limit = 0, $description = '') + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'addColumn', $project_id); + return $this->columnModel->create($project_id, $title, $task_limit, $description); + } + + public function removeColumn($column_id) + { + ColumnAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeColumn', $column_id); + return $this->columnModel->remove($column_id); + } + + public function changeColumnPosition($project_id, $column_id, $position) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'changeColumnPosition', $project_id); + return $this->columnModel->changePosition($project_id, $column_id, $position); + } +} diff --git a/sources/app/Api/Procedure/CommentProcedure.php b/sources/app/Api/Procedure/CommentProcedure.php new file mode 100644 index 0000000..019a49b --- /dev/null +++ b/sources/app/Api/Procedure/CommentProcedure.php @@ -0,0 +1,62 @@ +container)->check($this->getClassName(), 'getComment', $comment_id); + return $this->commentModel->getById($comment_id); + } + + public function getAllComments($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllComments', $task_id); + return $this->commentModel->getAll($task_id); + } + + public function removeComment($comment_id) + { + CommentAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeComment', $comment_id); + return $this->commentModel->remove($comment_id); + } + + public function createComment($task_id, $user_id, $content, $reference = '') + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createComment', $task_id); + + $values = array( + 'task_id' => $task_id, + 'user_id' => $user_id, + 'comment' => $content, + 'reference' => $reference, + ); + + list($valid, ) = $this->commentValidator->validateCreation($values); + + return $valid ? $this->commentModel->create($values) : false; + } + + public function updateComment($id, $content) + { + CommentAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateComment', $id); + + $values = array( + 'id' => $id, + 'comment' => $content, + ); + + list($valid, ) = $this->commentValidator->validateModification($values); + return $valid && $this->commentModel->update($values); + } +} diff --git a/sources/app/Api/Procedure/GroupMemberProcedure.php b/sources/app/Api/Procedure/GroupMemberProcedure.php new file mode 100644 index 0000000..081d6ac --- /dev/null +++ b/sources/app/Api/Procedure/GroupMemberProcedure.php @@ -0,0 +1,37 @@ +groupMemberModel->getGroups($user_id); + } + + public function getGroupMembers($group_id) + { + return $this->groupMemberModel->getMembers($group_id); + } + + public function addGroupMember($group_id, $user_id) + { + return $this->groupMemberModel->addUser($group_id, $user_id); + } + + public function removeGroupMember($group_id, $user_id) + { + return $this->groupMemberModel->removeUser($group_id, $user_id); + } + + public function isGroupMember($group_id, $user_id) + { + return $this->groupMemberModel->isMember($group_id, $user_id); + } +} diff --git a/sources/app/Api/Group.php b/sources/app/Api/Procedure/GroupProcedure.php similarity index 64% rename from sources/app/Api/Group.php rename to sources/app/Api/Procedure/GroupProcedure.php index a1e0a73..804940a 100644 --- a/sources/app/Api/Group.php +++ b/sources/app/Api/Procedure/GroupProcedure.php @@ -1,18 +1,18 @@ group->create($name, $external_id); + return $this->groupModel->create($name, $external_id); } public function updateGroup($group_id, $name = null, $external_id = null) @@ -29,21 +29,21 @@ class Group extends \Kanboard\Core\Base } } - return $this->group->update($values); + return $this->groupModel->update($values); } public function removeGroup($group_id) { - return $this->group->remove($group_id); + return $this->groupModel->remove($group_id); } public function getGroup($group_id) { - return $this->group->getById($group_id); + return $this->groupModel->getById($group_id); } public function getAllGroups() { - return $this->group->getAll(); + return $this->groupModel->getAll(); } } diff --git a/sources/app/Api/Link.php b/sources/app/Api/Procedure/LinkProcedure.php similarity index 78% rename from sources/app/Api/Link.php rename to sources/app/Api/Procedure/LinkProcedure.php index 23a9916..b4cecf3 100644 --- a/sources/app/Api/Link.php +++ b/sources/app/Api/Procedure/LinkProcedure.php @@ -1,14 +1,14 @@ link->getById($link_id); + return $this->linkModel->getById($link_id); } /** @@ -31,7 +31,7 @@ class Link extends \Kanboard\Core\Base */ public function getLinkByLabel($label) { - return $this->link->getByLabel($label); + return $this->linkModel->getByLabel($label); } /** @@ -43,7 +43,7 @@ class Link extends \Kanboard\Core\Base */ public function getOppositeLinkId($link_id) { - return $this->link->getOppositeLinkId($link_id); + return $this->linkModel->getOppositeLinkId($link_id); } /** @@ -54,7 +54,7 @@ class Link extends \Kanboard\Core\Base */ public function getAllLinks() { - return $this->link->getAll(); + return $this->linkModel->getAll(); } /** @@ -73,7 +73,7 @@ class Link extends \Kanboard\Core\Base ); list($valid, ) = $this->linkValidator->validateCreation($values); - return $valid ? $this->link->create($label, $opposite_label) : false; + return $valid ? $this->linkModel->create($label, $opposite_label) : false; } /** @@ -94,7 +94,7 @@ class Link extends \Kanboard\Core\Base ); list($valid, ) = $this->linkValidator->validateModification($values); - return $valid && $this->link->update($values); + return $valid && $this->linkModel->update($values); } /** @@ -106,6 +106,6 @@ class Link extends \Kanboard\Core\Base */ public function removeLink($link_id) { - return $this->link->remove($link_id); + return $this->linkModel->remove($link_id); } } diff --git a/sources/app/Api/Procedure/MeProcedure.php b/sources/app/Api/Procedure/MeProcedure.php new file mode 100644 index 0000000..e59e652 --- /dev/null +++ b/sources/app/Api/Procedure/MeProcedure.php @@ -0,0 +1,72 @@ +sessionStorage->user; + } + + public function getMyDashboard() + { + $user_id = $this->userSession->getId(); + $projects = $this->projectModel->getQueryColumnStats($this->projectPermissionModel->getActiveProjectIds($user_id))->findAll(); + $tasks = $this->taskFinderModel->getUserQuery($user_id)->findAll(); + + return array( + 'projects' => $this->formatProjects($projects), + 'tasks' => $this->formatTasks($tasks), + 'subtasks' => $this->subtaskModel->getUserQuery($user_id, array(SubtaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))->findAll(), + ); + } + + public function getMyActivityStream() + { + $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); + return $this->helper->projectActivity->getProjectsEvents($project_ids, 100); + } + + public function createMyPrivateProject($name, $description = null) + { + if ($this->configModel->get('disable_private_project', 0) == 1) { + return false; + } + + $values = array( + 'name' => $name, + 'description' => $description, + 'is_private' => 1, + ); + + list($valid, ) = $this->projectValidator->validateCreation($values); + return $valid ? $this->projectModel->create($values, $this->userSession->getId(), true) : false; + } + + public function getMyProjectsList() + { + return $this->projectUserRoleModel->getProjectsByUser($this->userSession->getId()); + } + + public function getMyOverdueTasks() + { + return $this->taskFinderModel->getOverdueTasksByUser($this->userSession->getId()); + } + + public function getMyProjects() + { + $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); + $projects = $this->projectModel->getAllByIds($project_ids); + + return $this->formatProjects($projects); + } +} diff --git a/sources/app/Api/Procedure/ProjectFileProcedure.php b/sources/app/Api/Procedure/ProjectFileProcedure.php new file mode 100644 index 0000000..48466ce --- /dev/null +++ b/sources/app/Api/Procedure/ProjectFileProcedure.php @@ -0,0 +1,68 @@ +container)->check($this->getClassName(), 'getProjectFile', $project_id); + return $this->projectFileModel->getById($file_id); + } + + public function getAllProjectFiles($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllProjectFiles', $project_id); + return $this->projectFileModel->getAll($project_id); + } + + public function downloadProjectFile($project_id, $file_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'downloadProjectFile', $project_id); + + try { + $file = $this->projectFileModel->getById($file_id); + + if (! empty($file)) { + return base64_encode($this->objectStorage->get($file['path'])); + } + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + + return ''; + } + + public function createProjectFile($project_id, $filename, $blob) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createProjectFile', $project_id); + + try { + return $this->projectFileModel->uploadContent($project_id, $filename, $blob); + } catch (ObjectStorageException $e) { + $this->logger->error(__METHOD__.': '.$e->getMessage()); + return false; + } + } + + public function removeProjectFile($project_id, $file_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProjectFile', $project_id); + return $this->projectFileModel->remove($file_id); + } + + public function removeAllProjectFiles($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeAllProjectFiles', $project_id); + return $this->projectFileModel->removeAll($project_id); + } +} diff --git a/sources/app/Api/Procedure/ProjectPermissionProcedure.php b/sources/app/Api/Procedure/ProjectPermissionProcedure.php new file mode 100644 index 0000000..e22e1d6 --- /dev/null +++ b/sources/app/Api/Procedure/ProjectPermissionProcedure.php @@ -0,0 +1,69 @@ +container)->check($this->getClassName(), 'getProjectUsers', $project_id); + return $this->projectUserRoleModel->getAllUsers($project_id); + } + + public function getAssignableUsers($project_id, $prepend_unassigned = false) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAssignableUsers', $project_id); + return $this->projectUserRoleModel->getAssignableUsersList($project_id, $prepend_unassigned); + } + + public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'addProjectUser', $project_id); + return $this->projectUserRoleModel->addUser($project_id, $user_id, $role); + } + + public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'addProjectGroup', $project_id); + return $this->projectGroupRoleModel->addGroup($project_id, $group_id, $role); + } + + public function removeProjectUser($project_id, $user_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProjectUser', $project_id); + return $this->projectUserRoleModel->removeUser($project_id, $user_id); + } + + public function removeProjectGroup($project_id, $group_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProjectGroup', $project_id); + return $this->projectGroupRoleModel->removeGroup($project_id, $group_id); + } + + public function changeProjectUserRole($project_id, $user_id, $role) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'changeProjectUserRole', $project_id); + return $this->projectUserRoleModel->changeUserRole($project_id, $user_id, $role); + } + + public function changeProjectGroupRole($project_id, $group_id, $role) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'changeProjectGroupRole', $project_id); + return $this->projectGroupRoleModel->changeGroupRole($project_id, $group_id, $role); + } + + public function getProjectUserRole($project_id, $user_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectUserRole', $project_id); + return $this->projectUserRoleModel->getUserRole($project_id, $user_id); + } +} diff --git a/sources/app/Api/Procedure/ProjectProcedure.php b/sources/app/Api/Procedure/ProjectProcedure.php new file mode 100644 index 0000000..a580c8d --- /dev/null +++ b/sources/app/Api/Procedure/ProjectProcedure.php @@ -0,0 +1,113 @@ +container)->check($this->getClassName(), 'getProjectById', $project_id); + return $this->formatProject($this->projectModel->getById($project_id)); + } + + public function getProjectByName($name) + { + $project = $this->projectModel->getByName($name); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectByName', $project['id']); + return $this->formatProject($project); + } + + public function getProjectByIdentifier($identifier) + { + $project = $this->formatProject($this->projectModel->getByIdentifier($identifier)); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectByIdentifier', $project['id']); + return $this->formatProject($project); + } + + public function getAllProjects() + { + return $this->formatProjects($this->projectModel->getAll()); + } + + public function removeProject($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeProject', $project_id); + return $this->projectModel->remove($project_id); + } + + public function enableProject($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'enableProject', $project_id); + return $this->projectModel->enable($project_id); + } + + public function disableProject($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'disableProject', $project_id); + return $this->projectModel->disable($project_id); + } + + public function enableProjectPublicAccess($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'enableProjectPublicAccess', $project_id); + return $this->projectModel->enablePublicAccess($project_id); + } + + public function disableProjectPublicAccess($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'disableProjectPublicAccess', $project_id); + return $this->projectModel->disablePublicAccess($project_id); + } + + public function getProjectActivities(array $project_ids) + { + foreach ($project_ids as $project_id) { + ProjectAuthorization::getInstance($this->container) + ->check($this->getClassName(), 'getProjectActivities', $project_id); + } + + return $this->helper->projectActivity->getProjectsEvents($project_ids); + } + + public function getProjectActivity($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getProjectActivity', $project_id); + return $this->helper->projectActivity->getProjectEvents($project_id); + } + + public function createProject($name, $description = null, $owner_id = 0, $identifier = null) + { + $values = $this->filterValues(array( + 'name' => $name, + 'description' => $description, + 'identifier' => $identifier, + )); + + list($valid, ) = $this->projectValidator->validateCreation($values); + return $valid ? $this->projectModel->create($values, $owner_id, $this->userSession->isLogged()) : false; + } + + public function updateProject($project_id, $name = null, $description = null, $owner_id = null, $identifier = null) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateProject', $project_id); + + $values = $this->filterValues(array( + 'id' => $project_id, + 'name' => $name, + 'description' => $description, + 'owner_id' => $owner_id, + 'identifier' => $identifier, + )); + + list($valid, ) = $this->projectValidator->validateModification($values); + return $valid && $this->projectModel->update($values); + } +} diff --git a/sources/app/Api/Subtask.php b/sources/app/Api/Procedure/SubtaskProcedure.php similarity index 55% rename from sources/app/Api/Subtask.php rename to sources/app/Api/Procedure/SubtaskProcedure.php index 782fdb0..e240091 100644 --- a/sources/app/Api/Subtask.php +++ b/sources/app/Api/Procedure/SubtaskProcedure.php @@ -1,32 +1,40 @@ subtask->getById($subtask_id); + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getSubtask', $subtask_id); + return $this->subtaskModel->getById($subtask_id); } public function getAllSubtasks($task_id) { - return $this->subtask->getAll($task_id); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllSubtasks', $task_id); + return $this->subtaskModel->getAll($task_id); } public function removeSubtask($subtask_id) { - return $this->subtask->remove($subtask_id); + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeSubtask', $subtask_id); + return $this->subtaskModel->remove($subtask_id); } public function createSubtask($task_id, $title, $user_id = 0, $time_estimated = 0, $time_spent = 0, $status = 0) { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createSubtask', $task_id); + $values = array( 'title' => $title, 'task_id' => $task_id, @@ -37,11 +45,13 @@ class Subtask extends \Kanboard\Core\Base ); list($valid, ) = $this->subtaskValidator->validateCreation($values); - return $valid ? $this->subtask->create($values) : false; + return $valid ? $this->subtaskModel->create($values) : false; } public function updateSubtask($id, $task_id, $title = null, $user_id = null, $time_estimated = null, $time_spent = null, $status = null) { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateSubtask', $task_id); + $values = array( 'id' => $id, 'task_id' => $task_id, @@ -59,6 +69,6 @@ class Subtask extends \Kanboard\Core\Base } list($valid, ) = $this->subtaskValidator->validateApiModification($values); - return $valid && $this->subtask->update($values); + return $valid && $this->subtaskModel->update($values); } } diff --git a/sources/app/Api/Procedure/SubtaskTimeTrackingProcedure.php b/sources/app/Api/Procedure/SubtaskTimeTrackingProcedure.php new file mode 100644 index 0000000..5ceaa08 --- /dev/null +++ b/sources/app/Api/Procedure/SubtaskTimeTrackingProcedure.php @@ -0,0 +1,39 @@ +container)->check($this->getClassName(), 'hasSubtaskTimer', $subtask_id); + return $this->subtaskTimeTrackingModel->hasTimer($subtask_id, $user_id); + } + + public function setSubtaskStartTime($subtask_id, $user_id) + { + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'setSubtaskStartTime', $subtask_id); + return $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $user_id); + } + + public function setSubtaskEndTime($subtask_id, $user_id) + { + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'setSubtaskEndTime', $subtask_id); + return $this->subtaskTimeTrackingModel->logEndTime($subtask_id, $user_id); + } + + public function getSubtaskTimeSpent($subtask_id, $user_id) + { + SubtaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getSubtaskTimeSpent', $subtask_id); + return $this->subtaskTimeTrackingModel->getTimeSpent($subtask_id, $user_id); + } +} diff --git a/sources/app/Api/Procedure/SwimlaneProcedure.php b/sources/app/Api/Procedure/SwimlaneProcedure.php new file mode 100644 index 0000000..9b7d181 --- /dev/null +++ b/sources/app/Api/Procedure/SwimlaneProcedure.php @@ -0,0 +1,91 @@ +container)->check($this->getClassName(), 'getActiveSwimlanes', $project_id); + return $this->swimlaneModel->getSwimlanes($project_id); + } + + public function getAllSwimlanes($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllSwimlanes', $project_id); + return $this->swimlaneModel->getAll($project_id); + } + + public function getSwimlaneById($swimlane_id) + { + $swimlane = $this->swimlaneModel->getById($swimlane_id); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getSwimlaneById', $swimlane['project_id']); + return $swimlane; + } + + public function getSwimlaneByName($project_id, $name) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getSwimlaneByName', $project_id); + return $this->swimlaneModel->getByName($project_id, $name); + } + + public function getSwimlane($swimlane_id) + { + return $this->swimlaneModel->getById($swimlane_id); + } + + public function getDefaultSwimlane($project_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getDefaultSwimlane', $project_id); + return $this->swimlaneModel->getDefault($project_id); + } + + public function addSwimlane($project_id, $name, $description = '') + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'addSwimlane', $project_id); + return $this->swimlaneModel->create(array('project_id' => $project_id, 'name' => $name, 'description' => $description)); + } + + public function updateSwimlane($swimlane_id, $name, $description = null) + { + $values = array('id' => $swimlane_id, 'name' => $name); + + if (!is_null($description)) { + $values['description'] = $description; + } + + return $this->swimlaneModel->update($values); + } + + public function removeSwimlane($project_id, $swimlane_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeSwimlane', $project_id); + return $this->swimlaneModel->remove($project_id, $swimlane_id); + } + + public function disableSwimlane($project_id, $swimlane_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'disableSwimlane', $project_id); + return $this->swimlaneModel->disable($project_id, $swimlane_id); + } + + public function enableSwimlane($project_id, $swimlane_id) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'enableSwimlane', $project_id); + return $this->swimlaneModel->enable($project_id, $swimlane_id); + } + + public function changeSwimlanePosition($project_id, $swimlane_id, $position) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'changeSwimlanePosition', $project_id); + return $this->swimlaneModel->changePosition($project_id, $swimlane_id, $position); + } +} diff --git a/sources/app/Api/Procedure/TaskExternalLinkProcedure.php b/sources/app/Api/Procedure/TaskExternalLinkProcedure.php new file mode 100644 index 0000000..05ec690 --- /dev/null +++ b/sources/app/Api/Procedure/TaskExternalLinkProcedure.php @@ -0,0 +1,106 @@ +externalLinkManager->getTypes(); + } + + public function getExternalTaskLinkProviderDependencies($providerName) + { + try { + return $this->externalLinkManager->getProvider($providerName)->getDependencies(); + } catch (ExternalLinkProviderNotFound $e) { + $this->logger->error(__METHOD__.': '.$e->getMessage()); + return false; + } + } + + public function getExternalTaskLinkById($task_id, $link_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getExternalTaskLink', $task_id); + return $this->taskExternalLinkModel->getById($link_id); + } + + public function getAllExternalTaskLinks($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getExternalTaskLinks', $task_id); + return $this->taskExternalLinkModel->getAll($task_id); + } + + public function createExternalTaskLink($task_id, $url, $dependency, $type = ExternalLinkManager::TYPE_AUTO, $title = '') + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createExternalTaskLink', $task_id); + + try { + $provider = $this->externalLinkManager + ->setUserInputText($url) + ->setUserInputType($type) + ->find(); + + $link = $provider->getLink(); + + $values = array( + 'task_id' => $task_id, + 'title' => $title ?: $link->getTitle(), + 'url' => $link->getUrl(), + 'link_type' => $provider->getType(), + 'dependency' => $dependency, + ); + + list($valid, $errors) = $this->externalLinkValidator->validateCreation($values); + + if (! $valid) { + $this->logger->error(__METHOD__.': '.var_export($errors)); + return false; + } + + return $this->taskExternalLinkModel->create($values); + } catch (ExternalLinkProviderNotFound $e) { + $this->logger->error(__METHOD__.': '.$e->getMessage()); + } + + return false; + } + + public function updateExternalTaskLink($task_id, $link_id, $title = null, $url = null, $dependency = null) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateExternalTaskLink', $task_id); + + $link = $this->taskExternalLinkModel->getById($link_id); + $values = $this->filterValues(array( + 'title' => $title, + 'url' => $url, + 'dependency' => $dependency, + )); + + $values = array_merge($link, $values); + list($valid, $errors) = $this->externalLinkValidator->validateModification($values); + + if (! $valid) { + $this->logger->error(__METHOD__.': '.var_export($errors)); + return false; + } + + return $this->taskExternalLinkModel->update($values); + } + + public function removeExternalTaskLink($task_id, $link_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeExternalTaskLink', $task_id); + return $this->taskExternalLinkModel->remove($link_id); + } +} diff --git a/sources/app/Api/Procedure/TaskFileProcedure.php b/sources/app/Api/Procedure/TaskFileProcedure.php new file mode 100644 index 0000000..bd00657 --- /dev/null +++ b/sources/app/Api/Procedure/TaskFileProcedure.php @@ -0,0 +1,70 @@ +container)->check($this->getClassName(), 'getTaskFile', $file_id); + return $this->taskFileModel->getById($file_id); + } + + public function getAllTaskFiles($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllTaskFiles', $task_id); + return $this->taskFileModel->getAll($task_id); + } + + public function downloadTaskFile($file_id) + { + TaskFileAuthorization::getInstance($this->container)->check($this->getClassName(), 'downloadTaskFile', $file_id); + + try { + $file = $this->taskFileModel->getById($file_id); + + if (! empty($file)) { + return base64_encode($this->objectStorage->get($file['path'])); + } + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + + return ''; + } + + public function createTaskFile($project_id, $task_id, $filename, $blob) + { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createTaskFile', $project_id); + + try { + return $this->taskFileModel->uploadContent($task_id, $filename, $blob); + } catch (ObjectStorageException $e) { + $this->logger->error(__METHOD__.': '.$e->getMessage()); + return false; + } + } + + public function removeTaskFile($file_id) + { + TaskFileAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeTaskFile', $file_id); + return $this->taskFileModel->remove($file_id); + } + + public function removeAllTaskFiles($task_id) + { + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeAllTaskFiles', $task_id); + return $this->taskFileModel->removeAll($task_id); + } +} diff --git a/sources/app/Api/TaskLink.php b/sources/app/Api/Procedure/TaskLinkProcedure.php similarity index 56% rename from sources/app/Api/TaskLink.php rename to sources/app/Api/Procedure/TaskLinkProcedure.php index 47d70d1..375266f 100644 --- a/sources/app/Api/TaskLink.php +++ b/sources/app/Api/Procedure/TaskLinkProcedure.php @@ -1,14 +1,17 @@ taskLink->getById($task_link_id); + TaskLinkAuthorization::getInstance($this->container)->check($this->getClassName(), 'getTaskLinkById', $task_link_id); + return $this->taskLinkModel->getById($task_link_id); } /** @@ -31,7 +35,8 @@ class TaskLink extends \Kanboard\Core\Base */ public function getAllTaskLinks($task_id) { - return $this->taskLink->getAll($task_id); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllTaskLinks', $task_id); + return $this->taskLinkModel->getAll($task_id); } /** @@ -45,7 +50,8 @@ class TaskLink extends \Kanboard\Core\Base */ public function createTaskLink($task_id, $opposite_task_id, $link_id) { - return $this->taskLink->create($task_id, $opposite_task_id, $link_id); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'createTaskLink', $task_id); + return $this->taskLinkModel->create($task_id, $opposite_task_id, $link_id); } /** @@ -60,7 +66,8 @@ class TaskLink extends \Kanboard\Core\Base */ 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); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateTaskLink', $task_id); + return $this->taskLinkModel->update($task_link_id, $task_id, $opposite_task_id, $link_id); } /** @@ -72,6 +79,7 @@ class TaskLink extends \Kanboard\Core\Base */ public function removeTaskLink($task_link_id) { - return $this->taskLink->remove($task_link_id); + TaskLinkAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeTaskLink', $task_link_id); + return $this->taskLinkModel->remove($task_link_id); } } diff --git a/sources/app/Api/Task.php b/sources/app/Api/Procedure/TaskProcedure.php similarity index 51% rename from sources/app/Api/Task.php rename to sources/app/Api/Procedure/TaskProcedure.php index 177a09c..8661dee 100644 --- a/sources/app/Api/Task.php +++ b/sources/app/Api/Procedure/TaskProcedure.php @@ -1,87 +1,99 @@ container)->check($this->getClassName(), 'searchTasks', $project_id); + return $this->taskLexer->build($query)->withFilter(new TaskProjectFilter($project_id))->toArray(); + } + public function getTask($task_id) { - $this->checkTaskPermission($task_id); - return $this->formatTask($this->taskFinder->getById($task_id)); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'getTask', $task_id); + return $this->formatTask($this->taskFinderModel->getById($task_id)); } public function getTaskByReference($project_id, $reference) { - $this->checkProjectPermission($project_id); - return $this->formatTask($this->taskFinder->getByReference($project_id, $reference)); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getTaskByReference', $project_id); + return $this->formatTask($this->taskFinderModel->getByReference($project_id, $reference)); } public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN) { - $this->checkProjectPermission($project_id); - return $this->formatTasks($this->taskFinder->getAll($project_id, $status_id)); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getAllTasks', $project_id); + return $this->formatTasks($this->taskFinderModel->getAll($project_id, $status_id)); } public function getOverdueTasks() { - return $this->taskFinder->getOverdueTasks(); + return $this->taskFinderModel->getOverdueTasks(); } public function getOverdueTasksByProject($project_id) { - $this->checkProjectPermission($project_id); - return $this->taskFinder->getOverdueTasksByProject($project_id); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'getOverdueTasksByProject', $project_id); + return $this->taskFinderModel->getOverdueTasksByProject($project_id); } public function openTask($task_id) { - $this->checkTaskPermission($task_id); - return $this->taskStatus->open($task_id); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'openTask', $task_id); + return $this->taskStatusModel->open($task_id); } public function closeTask($task_id) { - $this->checkTaskPermission($task_id); - return $this->taskStatus->close($task_id); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'closeTask', $task_id); + return $this->taskStatusModel->close($task_id); } public function removeTask($task_id) { - return $this->task->remove($task_id); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'removeTask', $task_id); + return $this->taskModel->remove($task_id); } public function moveTaskPosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0) { - $this->checkProjectPermission($project_id); - return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'moveTaskPosition', $project_id); + return $this->taskPositionModel->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id); } public function moveTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) { - return $this->taskDuplication->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'moveTaskToProject', $project_id); + return $this->taskProjectMoveModel->moveToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); } public function duplicateTaskToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) { - return $this->taskDuplication->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'duplicateTaskToProject', $project_id); + return $this->taskProjectDuplicationModel->duplicateToProject($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); } public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0, - $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, + $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, $priority = 0, $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, $recurrence_basedate = 0, $reference = '') { - $this->checkProjectPermission($project_id); - - if ($owner_id !== 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) { + ProjectAuthorization::getInstance($this->container)->check($this->getClassName(), 'createTask', $project_id); + + if ($owner_id !== 0 && ! $this->projectPermissionModel->isAssignable($project_id, $owner_id)) { return false; } @@ -107,31 +119,31 @@ class Task extends Base 'recurrence_timeframe' => $recurrence_timeframe, 'recurrence_basedate' => $recurrence_basedate, 'reference' => $reference, + 'priority' => $priority, ); list($valid, ) = $this->taskValidator->validateCreation($values); - return $valid ? $this->taskCreation->create($values) : false; + return $valid ? $this->taskCreationModel->create($values) : false; } public function updateTask($id, $title = null, $color_id = null, $owner_id = null, - $date_due = null, $description = null, $category_id = null, $score = null, + $date_due = null, $description = null, $category_id = null, $score = null, $priority = null, $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) { - $this->checkTaskPermission($id); - - $project_id = $this->taskFinder->getProjectId($id); + TaskAuthorization::getInstance($this->container)->check($this->getClassName(), 'updateTask', $id); + $project_id = $this->taskFinderModel->getProjectId($id); if ($project_id === 0) { return false; } - if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermission->isAssignable($project_id, $owner_id)) { + if ($owner_id !== null && $owner_id != 0 && ! $this->projectPermissionModel->isAssignable($project_id, $owner_id)) { return false; } - $values = array( + $values = $this->filterValues(array( 'id' => $id, 'title' => $title, 'color_id' => $color_id, @@ -146,15 +158,10 @@ class Task extends Base 'recurrence_timeframe' => $recurrence_timeframe, 'recurrence_basedate' => $recurrence_basedate, 'reference' => $reference, - ); - - foreach ($values as $key => $value) { - if (is_null($value)) { - unset($values[$key]); - } - } + 'priority' => $priority, + )); list($valid) = $this->taskValidator->validateApiModification($values); - return $valid && $this->taskModification->update($values); + return $valid && $this->taskModificationModel->update($values); } } diff --git a/sources/app/Api/User.php b/sources/app/Api/Procedure/UserProcedure.php similarity index 79% rename from sources/app/Api/User.php rename to sources/app/Api/Procedure/UserProcedure.php index 6ee935a..145f85b 100644 --- a/sources/app/Api/User.php +++ b/sources/app/Api/Procedure/UserProcedure.php @@ -1,6 +1,6 @@ user->getById($user_id); + return $this->userModel->getById($user_id); } public function getUserByName($username) { - return $this->user->getByUsername($username); + return $this->userModel->getByUsername($username); } public function getAllUsers() { - return $this->user->getAll(); + return $this->userModel->getAll(); } public function removeUser($user_id) { - return $this->user->remove($user_id); + return $this->userModel->remove($user_id); } public function disableUser($user_id) { - return $this->user->disable($user_id); + return $this->userModel->disable($user_id); } public function enableUser($user_id) { - return $this->user->enable($user_id); + return $this->userModel->enable($user_id); } public function isActiveUser($user_id) { - return $this->user->isActive($user_id); + return $this->userModel->isActive($user_id); } public function createUser($username, $password, $name = '', $email = '', $role = Role::APP_USER) @@ -63,7 +63,7 @@ class User extends \Kanboard\Core\Base ); list($valid, ) = $this->userValidator->validateCreation($values); - return $valid ? $this->user->create($values) : false; + return $valid ? $this->userModel->create($values) : false; } /** @@ -107,7 +107,7 @@ class User extends \Kanboard\Core\Base 'is_ldap_user' => 1, ); - return $this->user->create($values); + return $this->userModel->create($values); } catch (LdapException $e) { $this->logger->error($e->getMessage()); @@ -117,21 +117,15 @@ class User extends \Kanboard\Core\Base public function updateUser($id, $username = null, $name = null, $email = null, $role = null) { - $values = array( + $values = $this->filterValues(array( 'id' => $id, 'username' => $username, 'name' => $name, 'email' => $email, 'role' => $role, - ); - - foreach ($values as $key => $value) { - if (is_null($value)) { - unset($values[$key]); - } - } + )); list($valid, ) = $this->userValidator->validateApiModification($values); - return $valid && $this->user->update($values); + return $valid && $this->userModel->update($values); } } diff --git a/sources/app/Api/Project.php b/sources/app/Api/Project.php deleted file mode 100644 index 8e311f7..0000000 --- a/sources/app/Api/Project.php +++ /dev/null @@ -1,87 +0,0 @@ -checkProjectPermission($project_id); - return $this->formatProject($this->project->getById($project_id)); - } - - public function getProjectByName($name) - { - return $this->formatProject($this->project->getByName($name)); - } - - public function getAllProjects() - { - return $this->formatProjects($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) - { - $this->checkProjectPermission($project_id); - return $this->projectActivity->getProject($project_id); - } - - public function createProject($name, $description = null) - { - $values = array( - 'name' => $name, - 'description' => $description - ); - - list($valid, ) = $this->projectValidator->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->projectValidator->validateModification($values); - return $valid && $this->project->update($values); - } -} diff --git a/sources/app/Api/ProjectPermission.php b/sources/app/Api/ProjectPermission.php deleted file mode 100644 index 11e92af..0000000 --- a/sources/app/Api/ProjectPermission.php +++ /dev/null @@ -1,72 +0,0 @@ -projectUserRole->getAllUsers($project_id); - } - - public function getAssignableUsers($project_id, $prepend_unassigned = false) - { - return $this->projectUserRole->getAssignableUsersList($project_id, $prepend_unassigned); - } - - public function addProjectUser($project_id, $user_id, $role = Role::PROJECT_MEMBER) - { - return $this->projectUserRole->addUser($project_id, $user_id, $role); - } - - public function addProjectGroup($project_id, $group_id, $role = Role::PROJECT_MEMBER) - { - return $this->projectGroupRole->addGroup($project_id, $group_id, $role); - } - - public function removeProjectUser($project_id, $user_id) - { - return $this->projectUserRole->removeUser($project_id, $user_id); - } - - public function removeProjectGroup($project_id, $group_id) - { - return $this->projectGroupRole->removeGroup($project_id, $group_id); - } - - public function changeProjectUserRole($project_id, $user_id, $role) - { - return $this->projectUserRole->changeUserRole($project_id, $user_id, $role); - } - - public function changeProjectGroupRole($project_id, $group_id, $role) - { - return $this->projectGroupRole->changeGroupRole($project_id, $group_id, $role); - } - - // Deprecated - public function getMembers($project_id) - { - return $this->getProjectUsers($project_id); - } - - // Deprecated - public function revokeUser($project_id, $user_id) - { - return $this->removeProjectUser($project_id, $user_id); - } - - // Deprecated - public function allowUser($project_id, $user_id) - { - return $this->addProjectUser($project_id, $user_id); - } -} diff --git a/sources/app/Api/Swimlane.php b/sources/app/Api/Swimlane.php deleted file mode 100644 index 03a2819..0000000 --- a/sources/app/Api/Swimlane.php +++ /dev/null @@ -1,78 +0,0 @@ -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, $description = '') - { - return $this->swimlane->create(array('project_id' => $project_id, 'name' => $name, 'description' => $description)); - } - - public function updateSwimlane($swimlane_id, $name, $description = null) - { - $values = array('id' => $swimlane_id, 'name' => $name); - - if (!is_null($description)) { - $values['description'] = $description; - } - - return $this->swimlane->update($values); - } - - 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 changeSwimlanePosition($project_id, $swimlane_id, $position) - { - return $this->swimlane->changePosition($project_id, $swimlane_id, $position); - } -} diff --git a/sources/app/Auth/DatabaseAuth.php b/sources/app/Auth/DatabaseAuth.php index c13af68..ecb42c1 100644 --- a/sources/app/Auth/DatabaseAuth.php +++ b/sources/app/Auth/DatabaseAuth.php @@ -5,7 +5,7 @@ namespace Kanboard\Auth; use Kanboard\Core\Base; use Kanboard\Core\Security\PasswordAuthenticationProviderInterface; use Kanboard\Core\Security\SessionCheckProviderInterface; -use Kanboard\Model\User; +use Kanboard\Model\UserModel; use Kanboard\User\DatabaseUserProvider; /** @@ -60,7 +60,7 @@ class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterfa public function authenticate() { $user = $this->db - ->table(User::TABLE) + ->table(UserModel::TABLE) ->columns('id', 'password') ->eq('username', $this->username) ->eq('disable_login_form', 0) @@ -84,7 +84,7 @@ class DatabaseAuth extends Base implements PasswordAuthenticationProviderInterfa */ public function isValidSession() { - return $this->user->isActive($this->userSession->getId()); + return $this->userModel->isActive($this->userSession->getId()); } /** diff --git a/sources/app/Auth/LdapAuth.php b/sources/app/Auth/LdapAuth.php index c942358..a8dcfcb 100644 --- a/sources/app/Auth/LdapAuth.php +++ b/sources/app/Auth/LdapAuth.php @@ -76,7 +76,7 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME'); } - $this->logger->info('Authenticate user: '.$user->getDn()); + $this->logger->info('Authenticate this user: '.$user->getDn()); if ($client->authenticate($user->getDn(), $this->password)) { $this->userInfo = $user; diff --git a/sources/app/Auth/RememberMeAuth.php b/sources/app/Auth/RememberMeAuth.php index 509a511..5d0a8b2 100644 --- a/sources/app/Auth/RememberMeAuth.php +++ b/sources/app/Auth/RememberMeAuth.php @@ -44,16 +44,16 @@ class RememberMeAuth extends Base implements PreAuthenticationProviderInterface $credentials = $this->rememberMeCookie->read(); if ($credentials !== false) { - $session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']); + $session = $this->rememberMeSessionModel->find($credentials['token'], $credentials['sequence']); if (! empty($session)) { $this->rememberMeCookie->write( $session['token'], - $this->rememberMeSession->updateSequence($session['token']), + $this->rememberMeSessionModel->updateSequence($session['token']), $session['expiration'] ); - $this->userInfo = $this->user->getById($session['user_id']); + $this->userInfo = $this->userModel->getById($session['user_id']); return true; } diff --git a/sources/app/Auth/ReverseProxyAuth.php b/sources/app/Auth/ReverseProxyAuth.php index b9730c5..fdf936b 100644 --- a/sources/app/Auth/ReverseProxyAuth.php +++ b/sources/app/Auth/ReverseProxyAuth.php @@ -45,7 +45,8 @@ class ReverseProxyAuth extends Base implements PreAuthenticationProviderInterfac $username = $this->request->getRemoteUser(); if (! empty($username)) { - $this->userInfo = new ReverseProxyUserProvider($username); + $userProfile = $this->userModel->getByUsername($username); + $this->userInfo = new ReverseProxyUserProvider($username, $userProfile ?: array()); return true; } diff --git a/sources/app/Console/Base.php b/sources/app/Console/Base.php deleted file mode 100644 index 25d48e4..0000000 --- a/sources/app/Console/Base.php +++ /dev/null @@ -1,61 +0,0 @@ -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/sources/app/Console/BaseCommand.php b/sources/app/Console/BaseCommand.php new file mode 100644 index 0000000..5041707 --- /dev/null +++ b/sources/app/Console/BaseCommand.php @@ -0,0 +1,67 @@ +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/sources/app/Console/Cronjob.php b/sources/app/Console/CronjobCommand.php similarity index 95% rename from sources/app/Console/Cronjob.php rename to sources/app/Console/CronjobCommand.php index 3a5c559..dae13af 100644 --- a/sources/app/Console/Cronjob.php +++ b/sources/app/Console/CronjobCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\NullOutput; -class Cronjob extends Base +class CronjobCommand extends BaseCommand { private $commands = array( 'projects:daily-stats', diff --git a/sources/app/Console/LocaleComparator.php b/sources/app/Console/LocaleComparatorCommand.php similarity index 97% rename from sources/app/Console/LocaleComparator.php rename to sources/app/Console/LocaleComparatorCommand.php index 8e5e090..de83714 100644 --- a/sources/app/Console/LocaleComparator.php +++ b/sources/app/Console/LocaleComparatorCommand.php @@ -7,7 +7,7 @@ use RecursiveDirectoryIterator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class LocaleComparator extends Base +class LocaleComparatorCommand extends BaseCommand { const REF_LOCALE = 'fr_FR'; diff --git a/sources/app/Console/LocaleSync.php b/sources/app/Console/LocaleSyncCommand.php similarity index 97% rename from sources/app/Console/LocaleSync.php rename to sources/app/Console/LocaleSyncCommand.php index d62b40b..11cfbde 100644 --- a/sources/app/Console/LocaleSync.php +++ b/sources/app/Console/LocaleSyncCommand.php @@ -6,7 +6,7 @@ use DirectoryIterator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class LocaleSync extends Base +class LocaleSyncCommand extends BaseCommand { const REF_LOCALE = 'fr_FR'; diff --git a/sources/app/Console/PluginInstallCommand.php b/sources/app/Console/PluginInstallCommand.php new file mode 100644 index 0000000..a82f006 --- /dev/null +++ b/sources/app/Console/PluginInstallCommand.php @@ -0,0 +1,36 @@ +setName('plugin:install') + ->setDescription('Install a plugin from a remote Zip archive') + ->addArgument('url', InputArgument::REQUIRED, 'Archive URL'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!Installer::isConfigured()) { + throw new LogicException('Kanboard is not configured to install plugins itself'); + } + + try { + $installer = new Installer($this->container); + $installer->install($input->getArgument('url')); + $output->writeln('Plugin installed successfully'); + } catch (PluginInstallerException $e) { + $output->writeln(''.$e->getMessage().''); + } + } +} diff --git a/sources/app/Console/PluginUninstallCommand.php b/sources/app/Console/PluginUninstallCommand.php new file mode 100644 index 0000000..4872213 --- /dev/null +++ b/sources/app/Console/PluginUninstallCommand.php @@ -0,0 +1,36 @@ +setName('plugin:uninstall') + ->setDescription('Remove a plugin') + ->addArgument('pluginId', InputArgument::REQUIRED, 'Plugin directory name'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!Installer::isConfigured()) { + throw new LogicException('Kanboard is not configured to install plugins itself'); + } + + try { + $installer = new Installer($this->container); + $installer->uninstall($input->getArgument('pluginId')); + $output->writeln('Plugin removed successfully'); + } catch (PluginInstallerException $e) { + $output->writeln(''.$e->getMessage().''); + } + } +} diff --git a/sources/app/Console/PluginUpgradeCommand.php b/sources/app/Console/PluginUpgradeCommand.php new file mode 100644 index 0000000..6c66e91 --- /dev/null +++ b/sources/app/Console/PluginUpgradeCommand.php @@ -0,0 +1,55 @@ +setName('plugin:upgrade') + ->setDescription('Update all installed plugins') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!Installer::isConfigured()) { + throw new LogicException('Kanboard is not configured to install plugins itself'); + } + + $installer = new Installer($this->container); + $availablePlugins = Directory::getInstance($this->container)->getAvailablePlugins(); + + foreach ($this->pluginLoader->getPlugins() as $installedPlugin) { + $pluginDetails = $this->getPluginDetails($availablePlugins, $installedPlugin); + + if ($pluginDetails === null) { + $output->writeln('* Plugin not available in the directory: '.$installedPlugin->getPluginName().''); + } elseif ($pluginDetails['version'] > $installedPlugin->getPluginVersion()) { + $output->writeln('* Updating plugin: '.$installedPlugin->getPluginName().''); + $installer->update($pluginDetails['download']); + } else { + $output->writeln('* Plugin up to date: '.$installedPlugin->getPluginName().''); + } + } + } + + protected function getPluginDetails(array $availablePlugins, BasePlugin $installedPlugin) + { + foreach ($availablePlugins as $availablePlugin) { + if ($availablePlugin['title'] === $installedPlugin->getPluginName()) { + return $availablePlugin; + } + } + + return null; + } +} diff --git a/sources/app/Console/ProjectDailyColumnStatsExport.php b/sources/app/Console/ProjectDailyColumnStatsExportCommand.php similarity index 88% rename from sources/app/Console/ProjectDailyColumnStatsExport.php rename to sources/app/Console/ProjectDailyColumnStatsExportCommand.php index 2513fbf..1e8af72 100644 --- a/sources/app/Console/ProjectDailyColumnStatsExport.php +++ b/sources/app/Console/ProjectDailyColumnStatsExportCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ProjectDailyColumnStatsExport extends Base +class ProjectDailyColumnStatsExportCommand extends BaseCommand { protected function configure() { @@ -21,7 +21,7 @@ class ProjectDailyColumnStatsExport extends Base protected function execute(InputInterface $input, OutputInterface $output) { - $data = $this->projectDailyColumnStats->getAggregatedMetrics( + $data = $this->projectDailyColumnStatsModel->getAggregatedMetrics( $input->getArgument('project_id'), $input->getArgument('start_date'), $input->getArgument('end_date') diff --git a/sources/app/Console/ProjectDailyStatsCalculation.php b/sources/app/Console/ProjectDailyStatsCalculationCommand.php similarity index 60% rename from sources/app/Console/ProjectDailyStatsCalculation.php rename to sources/app/Console/ProjectDailyStatsCalculationCommand.php index 9884cc1..8dde8a7 100644 --- a/sources/app/Console/ProjectDailyStatsCalculation.php +++ b/sources/app/Console/ProjectDailyStatsCalculationCommand.php @@ -2,11 +2,11 @@ namespace Kanboard\Console; -use Kanboard\Model\Project; +use Kanboard\Model\ProjectModel; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ProjectDailyStatsCalculation extends Base +class ProjectDailyStatsCalculationCommand extends BaseCommand { protected function configure() { @@ -17,12 +17,12 @@ class ProjectDailyStatsCalculation extends Base protected function execute(InputInterface $input, OutputInterface $output) { - $projects = $this->project->getAllByStatus(Project::ACTIVE); + $projects = $this->projectModel->getAllByStatus(ProjectModel::ACTIVE); foreach ($projects as $project) { $output->writeln('Run calculation for '.$project['name']); - $this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d')); - $this->projectDailyStats->updateTotals($project['id'], date('Y-m-d')); + $this->projectDailyColumnStatsModel->updateTotals($project['id'], date('Y-m-d')); + $this->projectDailyStatsModel->updateTotals($project['id'], date('Y-m-d')); } } } diff --git a/sources/app/Console/ResetPasswordCommand.php b/sources/app/Console/ResetPasswordCommand.php new file mode 100644 index 0000000..b483f90 --- /dev/null +++ b/sources/app/Console/ResetPasswordCommand.php @@ -0,0 +1,79 @@ +setName('user:reset-password') + ->setDescription('Change user password') + ->addArgument('username', InputArgument::REQUIRED, 'Username') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $helper = $this->getHelper('question'); + $username = $input->getArgument('username'); + + $passwordQuestion = new Question('What is the new password for '.$username.'? (characters are not printed)'.PHP_EOL); + $passwordQuestion->setHidden(true); + $passwordQuestion->setHiddenFallback(false); + + $password = $helper->ask($input, $output, $passwordQuestion); + + $confirmationQuestion = new Question('Confirmation:'.PHP_EOL); + $confirmationQuestion->setHidden(true); + $confirmationQuestion->setHiddenFallback(false); + + $confirmation = $helper->ask($input, $output, $confirmationQuestion); + + if ($this->validatePassword($output, $password, $confirmation)) { + $this->resetPassword($output, $username, $password); + } + } + + private function validatePassword(OutputInterface $output, $password, $confirmation) + { + list($valid, $errors) = $this->passwordResetValidator->validateModification(array( + 'password' => $password, + 'confirmation' => $confirmation, + )); + + if (!$valid) { + foreach ($errors as $error_list) { + foreach ($error_list as $error) { + $output->writeln(''.$error.''); + } + } + } + + return $valid; + } + + private function resetPassword(OutputInterface $output, $username, $password) + { + $userId = $this->userModel->getIdByUsername($username); + + if (empty($userId)) { + $output->writeln('User not found'); + return false; + } + + if (!$this->userModel->update(array('id' => $userId, 'password' => $password))) { + $output->writeln('Unable to update password'); + return false; + } + + $output->writeln('Password updated successfully'); + + return true; + } +} diff --git a/sources/app/Console/ResetTwoFactorCommand.php b/sources/app/Console/ResetTwoFactorCommand.php new file mode 100644 index 0000000..a64206b --- /dev/null +++ b/sources/app/Console/ResetTwoFactorCommand.php @@ -0,0 +1,38 @@ +setName('user:reset-2fa') + ->setDescription('Remove two-factor authentication for a user') + ->addArgument('username', InputArgument::REQUIRED, 'Username'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $username = $input->getArgument('username'); + $userId = $this->userModel->getIdByUsername($username); + + if (empty($userId)) { + $output->writeln('User not found'); + return false; + } + + if (!$this->userModel->update(array('id' => $userId, 'twofactor_activated' => 0, 'twofactor_secret' => ''))) { + $output->writeln('Unable to update user profile'); + return false; + } + + $output->writeln('Two-factor authentication disabled'); + + return true; + } +} diff --git a/sources/app/Console/SubtaskExport.php b/sources/app/Console/SubtaskExportCommand.php similarity index 95% rename from sources/app/Console/SubtaskExport.php rename to sources/app/Console/SubtaskExportCommand.php index aaa9527..986af1a 100644 --- a/sources/app/Console/SubtaskExport.php +++ b/sources/app/Console/SubtaskExportCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class SubtaskExport extends Base +class SubtaskExportCommand extends BaseCommand { protected function configure() { diff --git a/sources/app/Console/TaskExport.php b/sources/app/Console/TaskExportCommand.php similarity index 95% rename from sources/app/Console/TaskExport.php rename to sources/app/Console/TaskExportCommand.php index 4515bf9..789245b 100644 --- a/sources/app/Console/TaskExport.php +++ b/sources/app/Console/TaskExportCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class TaskExport extends Base +class TaskExportCommand extends BaseCommand { protected function configure() { diff --git a/sources/app/Console/TaskOverdueNotification.php b/sources/app/Console/TaskOverdueNotification.php deleted file mode 100644 index 43be4df..0000000 --- a/sources/app/Console/TaskOverdueNotification.php +++ /dev/null @@ -1,116 +0,0 @@ -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->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(); - } - - /** - * Send overdue tasks - * - * @access public - */ - public function sendOverdueTaskNotifications() - { - $tasks = $this->taskFinder->getOverdueTasks(); - - foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) { - $users = $this->userNotification->getUsersWithNotificationEnabled($project_id); - - foreach ($users as $user) { - $this->sendUserOverdueTaskNotifications($user, $project_tasks); - } - } - - return $tasks; - } - - /** - * Send overdue tasks for a given user - * - * @access public - * @param array $user - * @param array $tasks - */ - public function sendUserOverdueTaskNotifications(array $user, array $tasks) - { - $user_tasks = array(); - - foreach ($tasks as $task) { - if ($this->userNotificationFilter->shouldReceiveNotification($user, array('task' => $task))) { - $user_tasks[] = $task; - } - } - - if (! empty($user_tasks)) { - $this->userNotification->sendUserNotification( - $user, - Task::EVENT_OVERDUE, - array('tasks' => $user_tasks, 'project_name' => $tasks[0]['project_name']) - ); - } - } - - /** - * 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/sources/app/Console/TaskOverdueNotificationCommand.php b/sources/app/Console/TaskOverdueNotificationCommand.php new file mode 100644 index 0000000..3627661 --- /dev/null +++ b/sources/app/Console/TaskOverdueNotificationCommand.php @@ -0,0 +1,191 @@ +setName('notification:overdue-tasks') + ->setDescription('Send notifications for overdue tasks') + ->addOption('show', null, InputOption::VALUE_NONE, 'Show sent overdue tasks') + ->addOption('group', null, InputOption::VALUE_NONE, 'Group all overdue tasks for one user (from all projects) in one email') + ->addOption('manager', null, InputOption::VALUE_NONE, 'Send all overdue tasks to project manager(s) in one email'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if ($input->getOption('group')) { + $tasks = $this->sendGroupOverdueTaskNotifications(); + } elseif ($input->getOption('manager')) { + $tasks = $this->sendOverdueTaskNotificationsToManagers(); + } else { + $tasks = $this->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(); + } + + /** + * Send all overdue tasks for one user in one email + * + * @access public + */ + public function sendGroupOverdueTaskNotifications() + { + $tasks = $this->taskFinderModel->getOverdueTasks(); + + foreach ($this->groupByColumn($tasks, 'owner_id') as $user_tasks) { + $users = $this->userNotificationModel->getUsersWithNotificationEnabled($user_tasks[0]['project_id']); + + foreach ($users as $user) { + $this->sendUserOverdueTaskNotifications($user, $user_tasks); + } + } + + return $tasks; + } + + /** + * Send all overdue tasks in one email to project manager(s) + * + * @access public + */ + public function sendOverdueTaskNotificationsToManagers() + { + $tasks = $this->taskFinderModel->getOverdueTasks(); + + foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) { + $users = $this->userNotificationModel->getUsersWithNotificationEnabled($project_id); + $managers = array(); + + foreach ($users as $user) { + $role = $this->projectUserRoleModel->getUserRole($project_id, $user['id']); + if($role == Role::PROJECT_MANAGER) { + $managers[] = $user; + } + } + + foreach ($managers as $manager) { + $this->sendUserOverdueTaskNotificationsToManagers($manager, $project_tasks); + } + } + + return $tasks; + } + + /** + * Send overdue tasks + * + * @access public + */ + public function sendOverdueTaskNotifications() + { + $tasks = $this->taskFinderModel->getOverdueTasks(); + + foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) { + $users = $this->userNotificationModel->getUsersWithNotificationEnabled($project_id); + + foreach ($users as $user) { + $this->sendUserOverdueTaskNotifications($user, $project_tasks); + } + } + + return $tasks; + } + + /** + * Send overdue tasks for a given user + * + * @access public + * @param array $user + * @param array $tasks + */ + public function sendUserOverdueTaskNotifications(array $user, array $tasks) + { + $user_tasks = array(); + $project_names = array(); + + foreach ($tasks as $task) { + if ($this->userNotificationFilterModel->shouldReceiveNotification($user, array('task' => $task))) { + $user_tasks[] = $task; + $project_names[$task['project_id']] = $task['project_name']; + } + } + + if (! empty($user_tasks)) { + $this->userNotificationModel->sendUserNotification( + $user, + TaskModel::EVENT_OVERDUE, + array('tasks' => $user_tasks, 'project_name' => implode(', ', $project_names)) + ); + } + } + + /** + * Send overdue tasks for a project manager(s) + * + * @access public + * @param array $manager + * @param array $tasks + */ + public function sendUserOverdueTaskNotificationsToManagers(array $manager, array $tasks) + { + $this->userNotificationModel->sendUserNotification( + $manager, + TaskModel::EVENT_OVERDUE, + array('tasks' => $tasks, 'project_name' => $tasks[0]['project_name']) + ); + } + + /** + * 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/sources/app/Console/TaskTrigger.php b/sources/app/Console/TaskTriggerCommand.php similarity index 79% rename from sources/app/Console/TaskTrigger.php rename to sources/app/Console/TaskTriggerCommand.php index 8d70721..a1f4dcc 100644 --- a/sources/app/Console/TaskTrigger.php +++ b/sources/app/Console/TaskTriggerCommand.php @@ -4,10 +4,10 @@ namespace Kanboard\Console; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; use Kanboard\Event\TaskListEvent; -class TaskTrigger extends Base +class TaskTriggerCommand extends BaseCommand { protected function configure() { @@ -19,7 +19,7 @@ class TaskTrigger extends Base protected function execute(InputInterface $input, OutputInterface $output) { foreach ($this->getProjectIds() as $project_id) { - $tasks = $this->taskFinder->getAll($project_id); + $tasks = $this->taskFinderModel->getAll($project_id); $nb_tasks = count($tasks); if ($nb_tasks > 0) { @@ -31,7 +31,7 @@ class TaskTrigger extends Base private function getProjectIds() { - $listeners = $this->dispatcher->getListeners(Task::EVENT_DAILY_CRONJOB); + $listeners = $this->dispatcher->getListeners(TaskModel::EVENT_DAILY_CRONJOB); $project_ids = array(); foreach ($listeners as $listener) { @@ -46,6 +46,6 @@ class TaskTrigger extends Base $event = new TaskListEvent(array('project_id' => $project_id)); $event->setTasks($tasks); - $this->dispatcher->dispatch(Task::EVENT_DAILY_CRONJOB, $event); + $this->dispatcher->dispatch(TaskModel::EVENT_DAILY_CRONJOB, $event); } } diff --git a/sources/app/Console/TransitionExport.php b/sources/app/Console/TransitionExportCommand.php similarity index 95% rename from sources/app/Console/TransitionExport.php rename to sources/app/Console/TransitionExportCommand.php index d9f805a..265757b 100644 --- a/sources/app/Console/TransitionExport.php +++ b/sources/app/Console/TransitionExportCommand.php @@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class TransitionExport extends Base +class TransitionExportCommand extends BaseCommand { protected function configure() { diff --git a/sources/app/Console/WorkerCommand.php b/sources/app/Console/WorkerCommand.php new file mode 100644 index 0000000..e332624 --- /dev/null +++ b/sources/app/Console/WorkerCommand.php @@ -0,0 +1,28 @@ +setName('worker') + ->setDescription('Execute queue worker') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->queueManager->listen(); + } +} diff --git a/sources/app/Controller/Action.php b/sources/app/Controller/ActionController.php similarity index 60% rename from sources/app/Controller/Action.php rename to sources/app/Controller/ActionController.php index 8881e8e..097640f 100644 --- a/sources/app/Controller/Action.php +++ b/sources/app/Controller/ActionController.php @@ -3,12 +3,12 @@ namespace Kanboard\Controller; /** - * Automatic Actions + * Automatic Actions Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Action extends Base +class ActionController extends BaseController { /** * List of automatic actions for a given project @@ -18,7 +18,7 @@ class Action extends Base public function index() { $project = $this->getProject(); - $actions = $this->action->getAllByProject($project['id']); + $actions = $this->actionModel->getAllByProject($project['id']); $this->response->html($this->helper->layout->project('action/index', array( 'values' => array('project_id' => $project['id']), @@ -27,12 +27,12 @@ class Action extends Base 'available_actions' => $this->actionManager->getAvailableActions(), 'available_events' => $this->eventManager->getAll(), 'available_params' => $this->actionManager->getAvailableParameters($actions), - 'columns_list' => $this->column->getList($project['id']), - 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), - 'projects_list' => $this->projectUserRole->getProjectsByUser($this->userSession->getId()), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($project['id']), - 'links_list' => $this->link->getList(0, false), + 'columns_list' => $this->columnModel->getList($project['id']), + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id']), + 'projects_list' => $this->projectUserRoleModel->getProjectsByUser($this->userSession->getId()), + 'colors_list' => $this->colorModel->getList(), + 'categories_list' => $this->categoryModel->getList($project['id']), + 'links_list' => $this->linkModel->getList(0, false), 'title' => t('Automatic actions') ))); } @@ -47,7 +47,7 @@ class Action extends Base $project = $this->getProject(); $this->response->html($this->helper->layout->project('action/remove', array( - 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), + 'action' => $this->actionModel->getById($this->request->getIntegerParam('action_id')), 'available_events' => $this->eventManager->getAll(), 'available_actions' => $this->actionManager->getAvailableActions(), 'project' => $project, @@ -64,14 +64,14 @@ class Action extends Base { $this->checkCSRFParam(); $project = $this->getProject(); - $action = $this->action->getById($this->request->getIntegerParam('action_id')); + $action = $this->actionModel->getById($this->request->getIntegerParam('action_id')); - if (! empty($action) && $this->action->remove($action['id'])) { + if (! empty($action) && $this->actionModel->remove($action['id'])) { $this->flash->success(t('Action removed successfully.')); } else { $this->flash->failure(t('Unable to remove this action.')); } - $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ActionController', 'index', array('project_id' => $project['id']))); } } diff --git a/sources/app/Controller/ActionCreation.php b/sources/app/Controller/ActionCreationController.php similarity index 73% rename from sources/app/Controller/ActionCreation.php rename to sources/app/Controller/ActionCreationController.php index 24a12d9..9b228f2 100644 --- a/sources/app/Controller/ActionCreation.php +++ b/sources/app/Controller/ActionCreationController.php @@ -3,12 +3,12 @@ namespace Kanboard\Controller; /** - * Action Creation + * Action Creation Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class ActionCreation extends Base +class ActionCreationController extends BaseController { /** * Show the form (step 1) @@ -40,7 +40,7 @@ class ActionCreation extends Base return $this->create(); } - $this->response->html($this->template->render('action_creation/event', array( + return $this->response->html($this->template->render('action_creation/event', array( 'values' => $values, 'project' => $project, 'available_actions' => $this->actionManager->getAvailableActions(), @@ -69,18 +69,19 @@ class ActionCreation extends Base $this->doCreation($project, $values + array('params' => array())); } - $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); + $projects_list = $this->projectUserRoleModel->getActiveProjectsByUser($this->userSession->getId()); unset($projects_list[$project['id']]); - $this->response->html($this->template->render('action_creation/params', array( + return $this->response->html($this->template->render('action_creation/params', array( 'values' => $values, 'action_params' => $action_params, - 'columns_list' => $this->column->getList($project['id']), - 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), + 'columns_list' => $this->columnModel->getList($project['id']), + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id']), 'projects_list' => $projects_list, - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($project['id']), - 'links_list' => $this->link->getList(0, false), + 'colors_list' => $this->colorModel->getList(), + 'categories_list' => $this->categoryModel->getList($project['id']), + 'links_list' => $this->linkModel->getList(0, false), + 'priorities_list' => $this->projectTaskPriorityModel->getPriorities($project), 'project' => $project, 'available_actions' => $this->actionManager->getAvailableActions(), 'events' => $this->actionManager->getCompatibleEvents($values['action_name']), @@ -109,13 +110,13 @@ class ActionCreation extends Base list($valid, ) = $this->actionValidator->validateCreation($values); if ($valid) { - if ($this->action->create($values) !== false) { + if ($this->actionModel->create($values) !== false) { $this->flash->success(t('Your automatic action have been created successfully.')); } else { $this->flash->failure(t('Unable to create your automatic action.')); } } - $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ActionController', 'index', array('project_id' => $project['id']))); } } diff --git a/sources/app/Controller/Activity.php b/sources/app/Controller/ActivityController.php similarity index 68% rename from sources/app/Controller/Activity.php rename to sources/app/Controller/ActivityController.php index e455b1d..9f9841a 100644 --- a/sources/app/Controller/Activity.php +++ b/sources/app/Controller/ActivityController.php @@ -3,12 +3,12 @@ namespace Kanboard\Controller; /** - * Activity stream + * Activity Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Activity extends Base +class ActivityController extends BaseController { /** * Activity page for a project @@ -20,7 +20,7 @@ class Activity extends Base $project = $this->getProject(); $this->response->html($this->helper->layout->app('activity/project', array( - 'events' => $this->projectActivity->getProject($project['id']), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id']), 'project' => $project, 'title' => t('%s\'s activity', $project['name']) ))); @@ -38,8 +38,8 @@ class Activity extends Base $this->response->html($this->helper->layout->task('activity/task', array( 'title' => $task['title'], 'task' => $task, - 'project' => $this->project->getById($task['project_id']), - 'events' => $this->projectActivity->getTask($task['id']), + 'project' => $this->projectModel->getById($task['project_id']), + 'events' => $this->helper->projectActivity->getTaskEvents($task['id']), ))); } } diff --git a/sources/app/Controller/Analytic.php b/sources/app/Controller/AnalyticController.php similarity index 83% rename from sources/app/Controller/Analytic.php rename to sources/app/Controller/AnalyticController.php index 6b0730b..cf3ba03 100644 --- a/sources/app/Controller/Analytic.php +++ b/sources/app/Controller/AnalyticController.php @@ -2,15 +2,16 @@ namespace Kanboard\Controller; -use Kanboard\Model\Task as TaskModel; +use Kanboard\Filter\TaskProjectFilter; +use Kanboard\Model\TaskModel; /** - * Project Analytic controller + * Project Analytic Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Analytic extends Base +class AnalyticController extends BaseController { /** * Show average Lead and Cycle time @@ -29,8 +30,8 @@ class Analytic extends Base ), 'project' => $project, 'average' => $this->averageLeadCycleTimeAnalytic->build($project['id']), - 'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to), - 'date_format' => $this->config->get('application_date_format'), + 'metrics' => $this->projectDailyStatsModel->getRawMetrics($project['id'], $from, $to), + 'date_format' => $this->configModel->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), 'title' => t('Lead and Cycle time for "%s"', $project['name']), ))); @@ -44,13 +45,15 @@ class Analytic extends Base public function compareHours() { $project = $this->getProject(); - $query = $this->taskFilter->create()->filterByProject($project['id'])->getQuery(); $paginator = $this->paginator - ->setUrl('analytic', 'compareHours', array('project_id' => $project['id'])) + ->setUrl('AnalyticController', 'compareHours', array('project_id' => $project['id'])) ->setMax(30) ->setOrder(TaskModel::TABLE.'.id') - ->setQuery($query) + ->setQuery($this->taskQuery + ->withFilter(new TaskProjectFilter($project['id'])) + ->getQuery() + ) ->calculate(); $this->response->html($this->helper->layout->analytic('analytic/compare_hours', array( @@ -142,7 +145,7 @@ class Analytic extends Base $project = $this->getProject(); list($from, $to) = $this->getDates(); - $display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2; + $display_graph = $this->projectDailyColumnStatsModel->countDays($project['id'], $from, $to) >= 2; $this->response->html($this->helper->layout->analytic($template, array( 'values' => array( @@ -150,9 +153,9 @@ class Analytic extends Base 'to' => $to, ), 'display_graph' => $display_graph, - 'metrics' => $display_graph ? $this->projectDailyColumnStats->getAggregatedMetrics($project['id'], $from, $to, $column) : array(), + 'metrics' => $display_graph ? $this->projectDailyColumnStatsModel->getAggregatedMetrics($project['id'], $from, $to, $column) : array(), 'project' => $project, - 'date_format' => $this->config->get('application_date_format'), + 'date_format' => $this->configModel->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), 'title' => t($title, $project['name']), ))); diff --git a/sources/app/Controller/AppController.php b/sources/app/Controller/AppController.php new file mode 100644 index 0000000..45cf39a --- /dev/null +++ b/sources/app/Controller/AppController.php @@ -0,0 +1,46 @@ +request->isAjax()) { + $this->response->json(array('message' => 'Access Forbidden'), 403); + } + + $this->response->html($this->helper->layout->app('app/forbidden', array( + 'title' => t('Access Forbidden'), + 'no_layout' => $withoutLayout, + ))); + } + + /** + * Page not found + * + * @access public + * @param boolean $withoutLayout + */ + public function notFound($withoutLayout = false) + { + $this->response->html($this->helper->layout->app('app/notfound', array( + 'title' => t('Page not found'), + 'no_layout' => $withoutLayout, + ))); + } +} diff --git a/sources/app/Controller/Auth.php b/sources/app/Controller/AuthController.php similarity index 61% rename from sources/app/Controller/Auth.php rename to sources/app/Controller/AuthController.php index b882a72..d1fba92 100644 --- a/sources/app/Controller/Auth.php +++ b/sources/app/Controller/AuthController.php @@ -3,12 +3,12 @@ namespace Kanboard\Controller; /** - * Authentication controller + * Authentication Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Auth extends Base +class AuthController extends BaseController { /** * Display the form login @@ -20,16 +20,16 @@ class Auth extends Base public function login(array $values = array(), array $errors = array()) { if ($this->userSession->isLogged()) { - $this->response->redirect($this->helper->url->to('app', 'index')); + $this->response->redirect($this->helper->url->to('DashboardController', 'show')); + } else { + $this->response->html($this->helper->layout->app('auth/index', array( + 'captcha' => ! empty($values['username']) && $this->userLockingModel->hasCaptcha($values['username']), + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + 'title' => t('Login') + ))); } - - $this->response->html($this->helper->layout->app('auth/index', array( - 'captcha' => ! empty($values['username']) && $this->userLocking->hasCaptcha($values['username']), - 'errors' => $errors, - 'values' => $values, - 'no_layout' => true, - 'title' => t('Login') - ))); } /** @@ -45,9 +45,9 @@ class Auth extends Base if ($valid) { $this->redirectAfterLogin(); + } else { + $this->login($values, $errors); } - - $this->login($values, $errors); } /** @@ -59,9 +59,9 @@ class Auth extends Base { if (! DISABLE_LOGOUT) { $this->sessionManager->close(); - $this->response->redirect($this->helper->url->to('auth', 'login')); + $this->response->redirect($this->helper->url->to('AuthController', 'login')); } else { - $this->response->redirect($this->helper->url->to('auth', 'index')); + $this->response->redirect($this->helper->url->to('DashboardController', 'show')); } } @@ -76,8 +76,8 @@ class Auth extends Base $redirect = $this->sessionStorage->redirectAfterLogin; unset($this->sessionStorage->redirectAfterLogin); $this->response->redirect($redirect); + } else { + $this->response->redirect($this->helper->url->to('DashboardController', 'show')); } - - $this->response->redirect($this->helper->url->to('app', 'index')); } } diff --git a/sources/app/Controller/AvatarFile.php b/sources/app/Controller/AvatarFileController.php similarity index 74% rename from sources/app/Controller/AvatarFile.php rename to sources/app/Controller/AvatarFileController.php index a47cca6..6879c57 100644 --- a/sources/app/Controller/AvatarFile.php +++ b/sources/app/Controller/AvatarFileController.php @@ -8,10 +8,10 @@ use Kanboard\Core\Thumbnail; /** * Avatar File Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class AvatarFile extends Base +class AvatarFileController extends BaseController { /** * Display avatar page @@ -32,11 +32,11 @@ class AvatarFile extends Base { $user = $this->getUser(); - if (! $this->avatarFile->uploadFile($user['id'], $this->request->getFileInfo('avatar'))) { + if (! $this->avatarFileModel->uploadImageFile($user['id'], $this->request->getFileInfo('avatar'))) { $this->flash->failure(t('Unable to upload the file.')); } - $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id']))); + $this->response->redirect($this->helper->url->to('AvatarFileController', 'show', array('user_id' => $user['id']))); } /** @@ -46,8 +46,9 @@ class AvatarFile extends Base { $this->checkCSRFParam(); $user = $this->getUser(); - $this->avatarFile->remove($user['id']); - $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id']))); + $this->avatarFileModel->remove($user['id']); + $this->userSession->refresh($user['id']); + $this->response->redirect($this->helper->url->to('AvatarFileController', 'show', array('user_id' => $user['id']))); } /** @@ -57,13 +58,14 @@ class AvatarFile extends Base { $user_id = $this->request->getIntegerParam('user_id'); $size = $this->request->getStringParam('size', 48); - $filename = $this->avatarFile->getFilename($user_id); + $filename = $this->avatarFileModel->getFilename($user_id); $etag = md5($filename.$size); - $this->response->cache(365 * 86400, $etag); - $this->response->contentType('image/jpeg'); + $this->response->withCache(365 * 86400, $etag); + $this->response->withContentType('image/jpeg'); if ($this->request->getHeader('If-None-Match') !== '"'.$etag.'"') { + $this->response->send(); $this->render($filename, $size); } else { $this->response->status(304); diff --git a/sources/app/Controller/Base.php b/sources/app/Controller/Base.php deleted file mode 100644 index beb5690..0000000 --- a/sources/app/Controller/Base.php +++ /dev/null @@ -1,290 +0,0 @@ -sessionManager->open(); - $this->dispatcher->dispatch('app.bootstrap'); - $this->sendHeaders(); - $this->authenticationManager->checkCurrentSession(); - - if (! $this->applicationAuthorization->isAllowed($this->router->getController(), $this->router->getAction(), Role::APP_PUBLIC)) { - $this->handleAuthentication(); - $this->handlePostAuthentication(); - $this->checkApplicationAuthorization(); - $this->checkProjectAuthorization(); - } - } - - /** - * Send HTTP headers - * - * @access private - */ - private function sendHeaders() - { - // HTTP secure headers - $this->response->csp($this->container['cspRules']); - $this->response->nosniff(); - $this->response->xss(); - - // Allow the public board iframe inclusion - if (ENABLE_XFRAME && $this->router->getAction() !== 'readonly') { - $this->response->xframe(); - } - - if (ENABLE_HSTS) { - $this->response->hsts(); - } - } - - /** - * Check authentication - * - * @access private - */ - private function handleAuthentication() - { - if (! $this->userSession->isLogged() && ! $this->authenticationManager->preAuthentication()) { - if ($this->request->isAjax()) { - $this->response->text('Not Authorized', 401); - } - - $this->sessionStorage->redirectAfterLogin = $this->request->getUri(); - $this->response->redirect($this->helper->url->to('auth', 'login')); - } - } - - /** - * Handle Post-Authentication (2FA) - * - * @access private - */ - private function handlePostAuthentication() - { - $controller = strtolower($this->router->getController()); - $action = strtolower($this->router->getAction()); - $ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout'); - - if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) { - if ($this->request->isAjax()) { - $this->response->text('Not Authorized', 401); - } - - $this->response->redirect($this->helper->url->to('twofactor', 'code')); - } - } - - /** - * Check application authorization - * - * @access private - */ - private function checkApplicationAuthorization() - { - if (! $this->helper->user->hasAccess($this->router->getController(), $this->router->getAction())) { - $this->forbidden(); - } - } - - /** - * Check project authorization - * - * @access private - */ - private function checkProjectAuthorization() - { - $project_id = $this->request->getIntegerParam('project_id'); - $task_id = $this->request->getIntegerParam('task_id'); - - // Allow urls without "project_id" - if ($task_id > 0 && $project_id === 0) { - $project_id = $this->taskFinder->getProjectId($task_id); - } - - if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($this->router->getController(), $this->router->getAction(), $project_id)) { - $this->forbidden(); - } - } - - /** - * Application not found page (404 error) - * - * @access protected - * @param boolean $no_layout Display the layout or not - */ - protected function notfound($no_layout = false) - { - $this->response->html($this->helper->layout->app('app/notfound', array( - 'title' => t('Page not found'), - 'no_layout' => $no_layout, - ))); - } - - /** - * Application forbidden page - * - * @access protected - * @param boolean $no_layout Display the layout or not - */ - protected function forbidden($no_layout = false) - { - if ($this->request->isAjax()) { - $this->response->text('Access Forbidden', 403); - } - - $this->response->html($this->helper->layout->app('app/forbidden', array( - 'title' => t('Access Forbidden'), - 'no_layout' => $no_layout, - ))); - } - - /** - * Check if the CSRF token from the URL is correct - * - * @access protected - */ - protected function checkCSRFParam() - { - if (! $this->token->validateCSRFToken($this->request->getStringParam('csrf_token'))) { - $this->forbidden(); - } - } - - /** - * Check webhook token - * - * @access protected - */ - protected function checkWebhookToken() - { - if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { - $this->response->text('Not Authorized', 401); - } - } - - /** - * Common method to get a task for task views - * - * @access protected - * @return array - */ - protected function getTask() - { - $project_id = $this->request->getIntegerParam('project_id'); - $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); - - if (empty($task)) { - $this->notfound(); - } - - if ($project_id !== 0 && $project_id != $task['project_id']) { - $this->forbidden(); - } - - return $task; - } - - /** - * Get Task or Project file - * - * @access protected - */ - protected function getFile() - { - $task_id = $this->request->getIntegerParam('task_id'); - $file_id = $this->request->getIntegerParam('file_id'); - $model = 'projectFile'; - - if ($task_id > 0) { - $model = 'taskFile'; - $project_id = $this->taskFinder->getProjectId($task_id); - - if ($project_id !== $this->request->getIntegerParam('project_id')) { - $this->forbidden(); - } - } - - $file = $this->$model->getById($file_id); - - if (empty($file)) { - $this->notfound(); - } - - $file['model'] = $model; - return $file; - } - - /** - * Common method to get a project - * - * @access protected - * @param integer $project_id Default project id - * @return array - */ - protected function getProject($project_id = 0) - { - $project_id = $this->request->getIntegerParam('project_id', $project_id); - $project = $this->project->getByIdWithOwner($project_id); - - if (empty($project)) { - $this->notfound(); - } - - return $project; - } - - /** - * Common method to get the user - * - * @access protected - * @return array - */ - protected function getUser() - { - $user = $this->user->getById($this->request->getIntegerParam('user_id', $this->userSession->getId())); - - if (empty($user)) { - $this->notfound(); - } - - if (! $this->userSession->isAdmin() && $this->userSession->getId() != $user['id']) { - $this->forbidden(); - } - - return $user; - } - - /** - * Get the current subtask - * - * @access protected - * @return array - */ - protected function getSubtask() - { - $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id')); - - if (empty($subtask)) { - $this->notfound(); - } - - return $subtask; - } -} diff --git a/sources/app/Controller/BaseController.php b/sources/app/Controller/BaseController.php new file mode 100644 index 0000000..5233e27 --- /dev/null +++ b/sources/app/Controller/BaseController.php @@ -0,0 +1,158 @@ +token->validateCSRFToken($this->request->getStringParam('csrf_token'))) { + throw new AccessForbiddenException(); + } + } + + /** + * Check webhook token + * + * @access protected + */ + protected function checkWebhookToken() + { + if ($this->configModel->get('webhook_token') !== $this->request->getStringParam('token')) { + throw AccessForbiddenException::getInstance()->withoutLayout(); + } + } + + /** + * Common method to get a task for task views + * + * @access protected + * @return array + * @throws PageNotFoundException + * @throws AccessForbiddenException + */ + protected function getTask() + { + $project_id = $this->request->getIntegerParam('project_id'); + $task = $this->taskFinderModel->getDetails($this->request->getIntegerParam('task_id')); + + if (empty($task)) { + throw new PageNotFoundException(); + } + + if ($project_id !== 0 && $project_id != $task['project_id']) { + throw new AccessForbiddenException(); + } + + return $task; + } + + /** + * Get Task or Project file + * + * @access protected + * @return array + * @throws PageNotFoundException + * @throws AccessForbiddenException + */ + protected function getFile() + { + $task_id = $this->request->getIntegerParam('task_id'); + $file_id = $this->request->getIntegerParam('file_id'); + $model = 'projectFileModel'; + + if ($task_id > 0) { + $model = 'taskFileModel'; + $project_id = $this->taskFinderModel->getProjectId($task_id); + + if ($project_id !== $this->request->getIntegerParam('project_id')) { + throw new AccessForbiddenException(); + } + } + + $file = $this->$model->getById($file_id); + + if (empty($file)) { + throw new PageNotFoundException(); + } + + $file['model'] = $model; + return $file; + } + + /** + * Common method to get a project + * + * @access protected + * @param integer $project_id Default project id + * @return array + * @throws PageNotFoundException + */ + protected function getProject($project_id = 0) + { + $project_id = $this->request->getIntegerParam('project_id', $project_id); + $project = $this->projectModel->getByIdWithOwner($project_id); + + if (empty($project)) { + throw new PageNotFoundException(); + } + + return $project; + } + + /** + * Common method to get the user + * + * @access protected + * @return array + * @throws PageNotFoundException + * @throws AccessForbiddenException + */ + protected function getUser() + { + $user = $this->userModel->getById($this->request->getIntegerParam('user_id', $this->userSession->getId())); + + if (empty($user)) { + throw new PageNotFoundException(); + } + + if (! $this->userSession->isAdmin() && $this->userSession->getId() != $user['id']) { + throw new AccessForbiddenException(); + } + + return $user; + } + + /** + * Get the current subtask + * + * @access protected + * @return array + * @throws PageNotFoundException + */ + protected function getSubtask() + { + $subtask = $this->subtaskModel->getById($this->request->getIntegerParam('subtask_id')); + + if (empty($subtask)) { + throw new PageNotFoundException(); + } + + return $subtask; + } +} diff --git a/sources/app/Controller/Board.php b/sources/app/Controller/Board.php deleted file mode 100644 index 51344bd..0000000 --- a/sources/app/Controller/Board.php +++ /dev/null @@ -1,186 +0,0 @@ -request->getStringParam('token'); - $project = $this->project->getByToken($token); - - // Token verification - if (empty($project)) { - $this->forbidden(true); - } - - // Display the board with a specific layout - $this->response->html($this->helper->layout->app('board/view_public', array( - 'project' => $project, - 'swimlanes' => $this->board->getBoard($project['id']), - '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'), - ))); - } - - /** - * Show a board for a given project - * - * @access public - */ - public function show() - { - $project = $this->getProject(); - $search = $this->helper->projectHeader->getSearchQuery($project); - - $this->response->html($this->helper->layout->app('board/view_private', array( - 'swimlanes' => $this->taskFilter->search($search)->getBoard($project['id']), - 'project' => $project, - 'title' => $project['name'], - 'description' => $this->helper->projectHeader->getDescription($project), - 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), - 'board_highlight_period' => $this->config->get('board_highlight_period'), - ))); - } - - /** - * Save the board (Ajax request made by the drag and drop) - * - * @access public - */ - public function save() - { - $project_id = $this->request->getIntegerParam('project_id'); - - if (! $project_id || ! $this->request->isAjax()) { - return $this->response->status(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); - } - - $this->response->html($this->renderBoard($project_id), 201); - } - - /** - * Check if the board have been changed - * - * @access public - */ - public function check() - { - $project_id = $this->request->getIntegerParam('project_id'); - $timestamp = $this->request->getIntegerParam('timestamp'); - - if (! $project_id || ! $this->request->isAjax()) { - return $this->response->status(403); - } - - if (! $this->project->isModifiedSince($project_id, $timestamp)) { - return $this->response->status(304); - } - - return $this->response->html($this->renderBoard($project_id)); - } - - /** - * Reload the board with new filters - * - * @access public - */ - public function reload() - { - $project_id = $this->request->getIntegerParam('project_id'); - - if (! $project_id || ! $this->request->isAjax()) { - return $this->response->status(403); - } - - $values = $this->request->getJson(); - $this->userSession->setFilters($project_id, empty($values['search']) ? '' : $values['search']); - - $this->response->html($this->renderBoard($project_id)); - } - - /** - * Enable collapsed mode - * - * @access public - */ - public function collapse() - { - $this->changeDisplayMode(true); - } - - /** - * Enable expanded mode - * - * @access public - */ - public function expand() - { - $this->changeDisplayMode(false); - } - - /** - * Change display mode - * - * @access private - * @param boolean $mode - */ - private function changeDisplayMode($mode) - { - $project_id = $this->request->getIntegerParam('project_id'); - $this->userSession->setBoardDisplayMode($project_id, $mode); - - if ($this->request->isAjax()) { - $this->response->html($this->renderBoard($project_id)); - } else { - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project_id))); - } - } - - /** - * Render board - * - * @access private - * @param integer $project_id - */ - private function renderBoard($project_id) - { - return $this->template->render('board/table_container', array( - 'project' => $this->project->getById($project_id), - 'swimlanes' => $this->taskFilter->search($this->userSession->getFilters($project_id))->getBoard($project_id), - 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), - 'board_highlight_period' => $this->config->get('board_highlight_period'), - )); - } -} diff --git a/sources/app/Controller/BoardAjaxController.php b/sources/app/Controller/BoardAjaxController.php new file mode 100644 index 0000000..9b721f0 --- /dev/null +++ b/sources/app/Controller/BoardAjaxController.php @@ -0,0 +1,140 @@ +request->getIntegerParam('project_id'); + + if (! $project_id || ! $this->request->isAjax()) { + throw new AccessForbiddenException(); + } + + $values = $this->request->getJson(); + + $result =$this->taskPositionModel->movePosition( + $project_id, + $values['task_id'], + $values['column_id'], + $values['position'], + $values['swimlane_id'] + ); + + if (! $result) { + $this->response->status(400); + } else { + $this->response->html($this->renderBoard($project_id), 201); + } + } + + /** + * Check if the board have been changed + * + * @access public + */ + public function check() + { + $project_id = $this->request->getIntegerParam('project_id'); + $timestamp = $this->request->getIntegerParam('timestamp'); + + if (! $project_id || ! $this->request->isAjax()) { + throw new AccessForbiddenException(); + } elseif (! $this->projectModel->isModifiedSince($project_id, $timestamp)) { + $this->response->status(304); + } else { + $this->response->html($this->renderBoard($project_id)); + } + } + + /** + * Reload the board with new filters + * + * @access public + */ + public function reload() + { + $project_id = $this->request->getIntegerParam('project_id'); + + if (! $project_id || ! $this->request->isAjax()) { + throw new AccessForbiddenException(); + } + + $values = $this->request->getJson(); + $this->userSession->setFilters($project_id, empty($values['search']) ? '' : $values['search']); + + $this->response->html($this->renderBoard($project_id)); + } + + /** + * Enable collapsed mode + * + * @access public + */ + public function collapse() + { + $this->changeDisplayMode(true); + } + + /** + * Enable expanded mode + * + * @access public + */ + public function expand() + { + $this->changeDisplayMode(false); + } + + /** + * Change display mode + * + * @access private + * @param boolean $mode + */ + private function changeDisplayMode($mode) + { + $project_id = $this->request->getIntegerParam('project_id'); + $this->userSession->setBoardDisplayMode($project_id, $mode); + + if ($this->request->isAjax()) { + $this->response->html($this->renderBoard($project_id)); + } else { + $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project_id))); + } + } + + /** + * Render board + * + * @access protected + * @param integer $project_id + * @return string + */ + protected function renderBoard($project_id) + { + return $this->template->render('board/table_container', array( + 'project' => $this->projectModel->getById($project_id), + 'board_private_refresh_interval' => $this->configModel->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->configModel->get('board_highlight_period'), + 'swimlanes' => $this->taskLexer + ->build($this->userSession->getFilters($project_id)) + ->format(BoardFormatter::getInstance($this->container)->withProjectId($project_id)) + )); + } +} diff --git a/sources/app/Controller/BoardPopover.php b/sources/app/Controller/BoardPopover.php deleted file mode 100644 index 63dab30..0000000 --- a/sources/app/Controller/BoardPopover.php +++ /dev/null @@ -1,135 +0,0 @@ -getTask(); - $project = $this->project->getById($task['project_id']); - - $this->response->html($this->template->render('board/popover_assignee', array( - 'values' => $task, - 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id']), - 'project' => $project, - ))); - } - - /** - * Validate an assignee modification - * - * @access public - */ - public function updateAssignee() - { - $values = $this->request->getValues(); - - list($valid, ) = $this->taskValidator->validateAssigneeModification($values); - - if ($valid && $this->taskModification->update($values)) { - $this->flash->success(t('Task updated successfully.')); - } else { - $this->flash->failure(t('Unable to update your task.')); - } - - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id']))); - } - - /** - * Change a task category directly from the board - * - * @access public - */ - public function changeCategory() - { - $task = $this->getTask(); - $project = $this->project->getById($task['project_id']); - - $this->response->html($this->template->render('board/popover_category', array( - 'values' => $task, - 'categories_list' => $this->category->getList($project['id']), - 'project' => $project, - ))); - } - - /** - * Validate a category modification - * - * @access public - */ - public function updateCategory() - { - $values = $this->request->getValues(); - - list($valid, ) = $this->taskValidator->validateCategoryModification($values); - - if ($valid && $this->taskModification->update($values)) { - $this->flash->success(t('Task updated successfully.')); - } else { - $this->flash->failure(t('Unable to update your task.')); - } - - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id']))); - } - - /** - * Screenshot popover - * - * @access public - */ - public function screenshot() - { - $task = $this->getTask(); - - $this->response->html($this->template->render('task_file/screenshot', array( - 'task' => $task, - ))); - } - - /** - * Confirmation before to close all column tasks - * - * @access public - */ - public function confirmCloseColumnTasks() - { - $project = $this->getProject(); - $column_id = $this->request->getIntegerParam('column_id'); - $swimlane_id = $this->request->getIntegerParam('swimlane_id'); - - $this->response->html($this->template->render('board/popover_close_all_tasks_column', array( - 'project' => $project, - 'nb_tasks' => $this->taskFinder->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id), - 'column' => $this->column->getColumnTitleById($column_id), - 'swimlane' => $this->swimlane->getNameById($swimlane_id) ?: t($project['default_swimlane']), - 'values' => array('column_id' => $column_id, 'swimlane_id' => $swimlane_id), - ))); - } - - /** - * Close all column tasks - * - * @access public - */ - public function closeColumnTasks() - { - $project = $this->getProject(); - $values = $this->request->getValues(); - - $this->taskStatus->closeTasksBySwimlaneAndColumn($values['swimlane_id'], $values['column_id']); - $this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->column->getColumnTitleById($values['column_id']), $this->swimlane->getNameById($values['swimlane_id']) ?: t($project['default_swimlane']))); - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id']))); - } -} diff --git a/sources/app/Controller/BoardPopoverController.php b/sources/app/Controller/BoardPopoverController.php new file mode 100644 index 0000000..a0f5ae1 --- /dev/null +++ b/sources/app/Controller/BoardPopoverController.php @@ -0,0 +1,47 @@ +getProject(); + $column_id = $this->request->getIntegerParam('column_id'); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->response->html($this->template->render('board_popover/close_all_tasks_column', array( + 'project' => $project, + 'nb_tasks' => $this->taskFinderModel->countByColumnAndSwimlaneId($project['id'], $column_id, $swimlane_id), + 'column' => $this->columnModel->getColumnTitleById($column_id), + 'swimlane' => $this->swimlaneModel->getNameById($swimlane_id) ?: t($project['default_swimlane']), + 'values' => array('column_id' => $column_id, 'swimlane_id' => $swimlane_id), + ))); + } + + /** + * Close all column tasks + * + * @access public + */ + public function closeColumnTasks() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $this->taskStatusModel->closeTasksBySwimlaneAndColumn($values['swimlane_id'], $values['column_id']); + $this->flash->success(t('All tasks of the column "%s" and the swimlane "%s" have been closed successfully.', $this->columnModel->getColumnTitleById($values['column_id']), $this->swimlaneModel->getNameById($values['swimlane_id']) ?: t($project['default_swimlane']))); + $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id']))); + } +} diff --git a/sources/app/Controller/BoardTooltip.php b/sources/app/Controller/BoardTooltipController.php similarity index 73% rename from sources/app/Controller/BoardTooltip.php rename to sources/app/Controller/BoardTooltipController.php index c7819bc..134d728 100644 --- a/sources/app/Controller/BoardTooltip.php +++ b/sources/app/Controller/BoardTooltipController.php @@ -5,10 +5,10 @@ namespace Kanboard\Controller; /** * Board Tooltip * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class BoardTooltip extends Base +class BoardTooltipController extends BaseController { /** * Get links on mouseover @@ -19,7 +19,7 @@ class BoardTooltip extends Base { $task = $this->getTask(); $this->response->html($this->template->render('board/tooltip_tasklinks', array( - 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), + 'links' => $this->taskLinkModel->getAllGroupedByLabel($task['id']), 'task' => $task, ))); } @@ -33,7 +33,7 @@ class BoardTooltip extends Base { $task = $this->getTask(); $this->response->html($this->template->render('board/tooltip_external_links', array( - 'links' => $this->taskExternalLink->getAll($task['id']), + 'links' => $this->taskExternalLinkModel->getAll($task['id']), 'task' => $task, ))); } @@ -47,7 +47,7 @@ class BoardTooltip extends Base { $task = $this->getTask(); $this->response->html($this->template->render('board/tooltip_subtasks', array( - 'subtasks' => $this->subtask->getAll($task['id']), + 'subtasks' => $this->subtaskModel->getAll($task['id']), 'task' => $task, ))); } @@ -62,7 +62,7 @@ class BoardTooltip extends Base $task = $this->getTask(); $this->response->html($this->template->render('board/tooltip_files', array( - 'files' => $this->taskFile->getAll($task['id']), + 'files' => $this->taskFileModel->getAll($task['id']), 'task' => $task, ))); } @@ -78,7 +78,7 @@ class BoardTooltip extends Base $this->response->html($this->template->render('board/tooltip_comments', array( 'task' => $task, - 'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting()) + 'comments' => $this->commentModel->getAll($task['id'], $this->userSession->getCommentSorting()) ))); } @@ -107,9 +107,9 @@ class BoardTooltip extends Base $this->response->html($this->template->render('task_recurrence/info', array( 'task' => $task, - 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), - 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), - 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + 'recurrence_trigger_list' => $this->taskRecurrenceModel->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->taskRecurrenceModel->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->taskRecurrenceModel->getRecurrenceBasedateList(), ))); } @@ -121,7 +121,7 @@ class BoardTooltip extends Base public function swimlane() { $this->getProject(); - $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id')); + $swimlane = $this->swimlaneModel->getById($this->request->getIntegerParam('swimlane_id')); $this->response->html($this->template->render('board/tooltip_description', array('task' => $swimlane))); } } diff --git a/sources/app/Controller/BoardViewController.php b/sources/app/Controller/BoardViewController.php new file mode 100644 index 0000000..97c99d1 --- /dev/null +++ b/sources/app/Controller/BoardViewController.php @@ -0,0 +1,69 @@ +request->getStringParam('token'); + $project = $this->projectModel->getByToken($token); + + if (empty($project)) { + throw AccessForbiddenException::getInstance()->withoutLayout(); + } + + $this->response->html($this->helper->layout->app('board/view_public', array( + 'project' => $project, + 'swimlanes' => BoardFormatter::getInstance($this->container) + ->withProjectId($project['id']) + ->withQuery($this->taskFinderModel->getExtendedQuery()) + ->format() + , + 'title' => $project['name'], + 'description' => $project['description'], + 'no_layout' => true, + 'not_editable' => true, + 'board_public_refresh_interval' => $this->configModel->get('board_public_refresh_interval'), + 'board_private_refresh_interval' => $this->configModel->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->configModel->get('board_highlight_period'), + ))); + } + + /** + * Show a board for a given project + * + * @access public + */ + public function show() + { + $project = $this->getProject(); + $search = $this->helper->projectHeader->getSearchQuery($project); + + $this->response->html($this->helper->layout->app('board/view_private', array( + 'project' => $project, + 'title' => $project['name'], + 'description' => $this->helper->projectHeader->getDescription($project), + 'board_private_refresh_interval' => $this->configModel->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->configModel->get('board_highlight_period'), + 'swimlanes' => $this->taskLexer + ->build($search) + ->format(BoardFormatter::getInstance($this->container)->withProjectId($project['id'])) + ))); + } +} diff --git a/sources/app/Controller/Calendar.php b/sources/app/Controller/CalendarController.php similarity index 50% rename from sources/app/Controller/Calendar.php rename to sources/app/Controller/CalendarController.php index af31ae4..e5114f0 100644 --- a/sources/app/Controller/Calendar.php +++ b/sources/app/Controller/CalendarController.php @@ -2,16 +2,19 @@ namespace Kanboard\Controller; -use Kanboard\Model\Task as TaskModel; +use Kanboard\Filter\TaskAssigneeFilter; +use Kanboard\Filter\TaskProjectFilter; +use Kanboard\Filter\TaskStatusFilter; +use Kanboard\Model\TaskModel; /** - * Project Calendar controller + * Calendar Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot * @author Timo Litzbarski */ -class Calendar extends Base +class CalendarController extends BaseController { /** * Show calendar view for projects @@ -26,7 +29,7 @@ class Calendar extends Base 'project' => $project, 'title' => $project['name'], 'description' => $this->helper->projectHeader->getDescription($project), - 'check_interval' => $this->config->get('board_private_refresh_interval'), + 'check_interval' => $this->configModel->get('board_private_refresh_interval'), ))); } @@ -40,21 +43,11 @@ class Calendar extends Base $project_id = $this->request->getIntegerParam('project_id'); $start = $this->request->getStringParam('start'); $end = $this->request->getStringParam('end'); + $search = $this->userSession->getFilters($project_id); + $queryBuilder = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project_id)); - // Common filter - $filter = $this->taskFilterCalendarFormatter - ->search($this->userSession->getFilters($project_id)) - ->filterByProject($project_id); - - // Tasks - if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { - $events = $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format(); - } else { - $events = $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format(); - } - - // Tasks with due date - $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format()); + $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end); + $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end)); $events = $this->hook->merge('controller:calendar:project:events', $events, array( 'project_id' => $project_id, @@ -75,21 +68,15 @@ class Calendar extends Base $user_id = $this->request->getIntegerParam('user_id'); $start = $this->request->getStringParam('start'); $end = $this->request->getStringParam('end'); - $filter = $this->taskFilterCalendarFormatter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN); + $queryBuilder = $this->taskQuery + ->withFilter(new TaskAssigneeFilter($user_id)) + ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN)); - // Task with due date - $events = $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format(); + $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end); + $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end)); - // Tasks - if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') { - $events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format()); - } else { - $events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format()); - } - - // Subtasks time tracking - if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) { - $events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end)); + if ($this->configModel->get('calendar_user_subtasks_time_tracking') == 1) { + $events = array_merge($events, $this->helper->calendar->getSubtaskTimeTrackingEvents($user_id, $start, $end)); } $events = $this->hook->merge('controller:calendar:user:events', $events, array( @@ -111,7 +98,7 @@ class Calendar extends Base if ($this->request->isAjax() && $this->request->isPost()) { $values = $this->request->getJson(); - $this->taskModification->update(array( + $this->taskModificationModel->update(array( 'id' => $values['task_id'], 'date_due' => substr($values['date_due'], 0, 10), )); diff --git a/sources/app/Controller/Captcha.php b/sources/app/Controller/CaptchaController.php similarity index 74% rename from sources/app/Controller/Captcha.php rename to sources/app/Controller/CaptchaController.php index fcf081e..43b2f82 100644 --- a/sources/app/Controller/Captcha.php +++ b/sources/app/Controller/CaptchaController.php @@ -7,10 +7,10 @@ use Gregwar\Captcha\CaptchaBuilder; /** * Captcha Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Captcha extends Base +class CaptchaController extends BaseController { /** * Display captcha image @@ -19,7 +19,7 @@ class Captcha extends Base */ public function image() { - $this->response->contentType('image/jpeg'); + $this->response->withContentType('image/jpeg')->send(); $builder = new CaptchaBuilder; $builder->build(); diff --git a/sources/app/Controller/Category.php b/sources/app/Controller/CategoryController.php similarity index 68% rename from sources/app/Controller/Category.php rename to sources/app/Controller/CategoryController.php index 258a3b7..dd6e1c3 100644 --- a/sources/app/Controller/Category.php +++ b/sources/app/Controller/CategoryController.php @@ -2,28 +2,29 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\PageNotFoundException; + /** - * Category management + * Category Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Category extends Base +class CategoryController extends BaseController { /** * Get the category (common method between actions) * * @access private - * @param integer $project_id * @return array + * @throws PageNotFoundException */ - private function getCategory($project_id) + private function getCategory() { - $category = $this->category->getById($this->request->getIntegerParam('category_id')); + $category = $this->categoryModel->getById($this->request->getIntegerParam('category_id')); if (empty($category)) { - $this->flash->failure(t('Category not found.')); - $this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project_id))); + throw new PageNotFoundException(); } return $category; @@ -33,13 +34,16 @@ class Category extends Base * List of categories for a given project * * @access public + * @param array $values + * @param array $errors + * @throws PageNotFoundException */ public function index(array $values = array(), array $errors = array()) { $project = $this->getProject(); $this->response->html($this->helper->layout->project('category/index', array( - 'categories' => $this->category->getList($project['id'], false), + 'categories' => $this->categoryModel->getList($project['id'], false), 'values' => $values + array('project_id' => $project['id']), 'errors' => $errors, 'project' => $project, @@ -60,26 +64,29 @@ class Category extends Base list($valid, $errors) = $this->categoryValidator->validateCreation($values); if ($valid) { - if ($this->category->create($values)) { + if ($this->categoryModel->create($values) !== false) { $this->flash->success(t('Your category have been created successfully.')); - $this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id']))); + return $this->response->redirect($this->helper->url->to('CategoryController', 'index', array('project_id' => $project['id']))); } else { $this->flash->failure(t('Unable to create your category.')); } } - $this->index($values, $errors); + return $this->index($values, $errors); } /** * Edit a category (display the form) * * @access public + * @param array $values + * @param array $errors + * @throws PageNotFoundException */ public function edit(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $category = $this->getCategory($project['id']); + $category = $this->getCategory(); $this->response->html($this->helper->layout->project('category/edit', array( 'values' => empty($values) ? $category : $values, @@ -102,15 +109,15 @@ class Category extends Base list($valid, $errors) = $this->categoryValidator->validateModification($values); if ($valid) { - if ($this->category->update($values)) { + if ($this->categoryModel->update($values)) { $this->flash->success(t('Your category have been updated successfully.')); - $this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id']))); + return $this->response->redirect($this->helper->url->to('CategoryController', 'index', array('project_id' => $project['id']))); } else { $this->flash->failure(t('Unable to update your category.')); } } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** @@ -121,7 +128,7 @@ class Category extends Base public function confirm() { $project = $this->getProject(); - $category = $this->getCategory($project['id']); + $category = $this->getCategory(); $this->response->html($this->helper->layout->project('category/remove', array( 'project' => $project, @@ -139,14 +146,14 @@ class Category extends Base { $this->checkCSRFParam(); $project = $this->getProject(); - $category = $this->getCategory($project['id']); + $category = $this->getCategory(); - if ($this->category->remove($category['id'])) { + if ($this->categoryModel->remove($category['id'])) { $this->flash->success(t('Category removed successfully.')); } else { $this->flash->failure(t('Unable to remove this category.')); } - $this->response->redirect($this->helper->url->to('category', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('CategoryController', 'index', array('project_id' => $project['id']))); } } diff --git a/sources/app/Controller/Column.php b/sources/app/Controller/ColumnController.php similarity index 70% rename from sources/app/Controller/Column.php rename to sources/app/Controller/ColumnController.php index bbe12fe..95fbcaa 100644 --- a/sources/app/Controller/Column.php +++ b/sources/app/Controller/ColumnController.php @@ -2,13 +2,15 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\AccessForbiddenException; + /** - * Column controller + * Column Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Column extends Base +class ColumnController extends BaseController { /** * Display columns list @@ -18,7 +20,7 @@ class Column extends Base public function index() { $project = $this->getProject(); - $columns = $this->column->getAll($project['id']); + $columns = $this->columnModel->getAll($project['id']); $this->response->html($this->helper->layout->project('column/index', array( 'columns' => $columns, @@ -31,6 +33,9 @@ class Column extends Base * Show form to create a new column * * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\PageNotFoundException */ public function create(array $values = array(), array $errors = array()) { @@ -61,15 +66,15 @@ class Column extends Base list($valid, $errors) = $this->columnValidator->validateCreation($values); if ($valid) { - if ($this->column->create($project['id'], $values['title'], $values['task_limit'], $values['description'])) { + if ($this->columnModel->create($project['id'], $values['title'], $values['task_limit'], $values['description']) !== false) { $this->flash->success(t('Column created successfully.')); - return $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id'])), true); + return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id'])), true); } else { $errors['title'] = array(t('Another column with the same name exists in the project')); } } - $this->create($values, $errors); + return $this->create($values, $errors); } /** @@ -82,7 +87,7 @@ class Column extends Base public function edit(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $column = $this->column->getById($this->request->getIntegerParam('column_id')); + $column = $this->columnModel->getById($this->request->getIntegerParam('column_id')); $this->response->html($this->helper->layout->project('column/edit', array( 'errors' => $errors, @@ -106,15 +111,15 @@ class Column extends Base list($valid, $errors) = $this->columnValidator->validateModification($values); if ($valid) { - if ($this->column->update($values['id'], $values['title'], $values['task_limit'], $values['description'])) { + if ($this->columnModel->update($values['id'], $values['title'], $values['task_limit'], $values['description'])) { $this->flash->success(t('Board updated successfully.')); - $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); + return $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id']))); } else { $this->flash->failure(t('Unable to update this board.')); } } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** @@ -128,11 +133,11 @@ class Column extends Base $values = $this->request->getJson(); if (! empty($values) && isset($values['column_id']) && isset($values['position'])) { - $result = $this->column->changePosition($project['id'], $values['column_id'], $values['position']); - return $this->response->json(array('result' => $result)); + $result = $this->columnModel->changePosition($project['id'], $values['column_id'], $values['position']); + $this->response->json(array('result' => $result)); + } else { + throw new AccessForbiddenException(); } - - $this->forbidden(); } /** @@ -145,7 +150,7 @@ class Column extends Base $project = $this->getProject(); $this->response->html($this->helper->layout->project('column/remove', array( - 'column' => $this->column->getById($this->request->getIntegerParam('column_id')), + 'column' => $this->columnModel->getById($this->request->getIntegerParam('column_id')), 'project' => $project, 'title' => t('Remove a column from a board') ))); @@ -162,12 +167,12 @@ class Column extends Base $this->checkCSRFParam(); $column_id = $this->request->getIntegerParam('column_id'); - if ($this->column->remove($column_id)) { + if ($this->columnModel->remove($column_id)) { $this->flash->success(t('Column removed successfully.')); } else { $this->flash->failure(t('Unable to remove this column.')); } - $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ColumnController', 'index', array('project_id' => $project['id']))); } } diff --git a/sources/app/Controller/Comment.php b/sources/app/Controller/CommentController.php similarity index 70% rename from sources/app/Controller/Comment.php rename to sources/app/Controller/CommentController.php index ff7ec30..2a8c258 100644 --- a/sources/app/Controller/Comment.php +++ b/sources/app/Controller/CommentController.php @@ -2,30 +2,35 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\AccessForbiddenException; +use Kanboard\Core\Controller\PageNotFoundException; + /** - * Comment controller + * Comment Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Comment extends Base +class CommentController extends BaseController { /** * Get the current comment * * @access private * @return array + * @throws PageNotFoundException + * @throws AccessForbiddenException */ private function getComment() { - $comment = $this->comment->getById($this->request->getIntegerParam('comment_id')); + $comment = $this->commentModel->getById($this->request->getIntegerParam('comment_id')); if (empty($comment)) { - return $this->notfound(); + throw new PageNotFoundException(); } if (! $this->userSession->isAdmin() && $comment['user_id'] != $this->userSession->getId()) { - return $this->forbidden(); + throw new AccessForbiddenException(); } return $comment; @@ -35,6 +40,10 @@ class Comment extends Base * Add comment form * * @access public + * @param array $values + * @param array $errors + * @throws AccessForbiddenException + * @throws PageNotFoundException */ public function create(array $values = array(), array $errors = array()) { @@ -67,22 +76,26 @@ class Comment extends Base list($valid, $errors) = $this->commentValidator->validateCreation($values); if ($valid) { - if ($this->comment->create($values)) { + if ($this->commentModel->create($values) !== false) { $this->flash->success(t('Comment added successfully.')); } else { $this->flash->failure(t('Unable to create your comment.')); } - return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments'), true); } - $this->create($values, $errors); + return $this->create($values, $errors); } /** * Edit a comment * * @access public + * @param array $values + * @param array $errors + * @throws AccessForbiddenException + * @throws PageNotFoundException */ public function edit(array $values = array(), array $errors = array()) { @@ -112,16 +125,16 @@ class Comment extends Base list($valid, $errors) = $this->commentValidator->validateModification($values); if ($valid) { - if ($this->comment->update($values)) { + if ($this->commentModel->update($values)) { $this->flash->success(t('Comment updated successfully.')); } else { $this->flash->failure(t('Unable to update your comment.')); } - return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), false); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), false); } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** @@ -152,13 +165,13 @@ class Comment extends Base $task = $this->getTask(); $comment = $this->getComment(); - if ($this->comment->remove($comment['id'])) { + if ($this->commentModel->remove($comment['id'])) { $this->flash->success(t('Comment removed successfully.')); } else { $this->flash->failure(t('Unable to remove this comment.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments')); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments')); } /** @@ -173,6 +186,6 @@ class Comment extends Base $order = $this->userSession->getCommentSorting() === 'ASC' ? 'DESC' : 'ASC'; $this->userSession->setCommentSorting($order); - $this->response->redirect($this->helper->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'comments')); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), 'comments')); } } diff --git a/sources/app/Controller/Config.php b/sources/app/Controller/ConfigController.php similarity index 59% rename from sources/app/Controller/Config.php rename to sources/app/Controller/ConfigController.php index a1b8c2a..8285ee1 100644 --- a/sources/app/Controller/Config.php +++ b/sources/app/Controller/ConfigController.php @@ -3,55 +3,13 @@ namespace Kanboard\Controller; /** - * Config controller + * Config Controller * - * @package controller + * @package Kanboard/Controller * @author Frederic Guillot */ -class Config extends Base +class ConfigController extends BaseController { - /** - * Common method between pages - * - * @access private - * @param string $redirect Action to redirect after saving the form - */ - private function common($redirect) - { - if ($this->request->isPost()) { - $values = $this->request->getValues(); - - switch ($redirect) { - case 'application': - $values += array('password_reset' => 0); - break; - case 'project': - $values += array( - 'subtask_restriction' => 0, - 'subtask_time_tracking' => 0, - 'cfd_include_closed_tasks' => 0, - 'disable_private_project' => 0, - ); - break; - case 'integrations': - $values += array('integration_gravatar' => 0); - break; - case 'calendar': - $values += array('calendar_user_subtasks_time_tracking' => 0); - break; - } - - if ($this->config->save($values)) { - $this->config->reload(); - $this->flash->success(t('Settings saved successfully.')); - } else { - $this->flash->failure(t('Unable to save your settings.')); - } - - $this->response->redirect($this->helper->url->to('config', $redirect)); - } - } - /** * Display the about page * @@ -60,7 +18,7 @@ class Config extends Base public function index() { $this->response->html($this->helper->layout->config('config/about', array( - 'db_size' => $this->config->getDatabaseSize(), + 'db_size' => $this->configModel->getDatabaseSize(), 'db_version' => $this->db->getDriver()->getDatabaseVersion(), 'user_agent' => $this->request->getServerVariable('HTTP_USER_AGENT'), 'title' => t('Settings').' > '.t('About'), @@ -68,16 +26,42 @@ class Config extends Base } /** - * Display the plugin page + * Save settings * - * @access public */ - public function plugins() + public function save() { - $this->response->html($this->helper->layout->config('config/plugins', array( - 'plugins' => $this->pluginLoader->plugins, - 'title' => t('Settings').' > '.t('Plugins'), - ))); + $values = $this->request->getValues(); + $redirect = $this->request->getStringParam('redirect', 'application'); + + switch ($redirect) { + case 'application': + $values += array('password_reset' => 0); + break; + case 'project': + $values += array( + 'subtask_restriction' => 0, + 'subtask_time_tracking' => 0, + 'cfd_include_closed_tasks' => 0, + 'disable_private_project' => 0, + ); + break; + case 'integrations': + $values += array('integration_gravatar' => 0); + break; + case 'calendar': + $values += array('calendar_user_subtasks_time_tracking' => 0); + break; + } + + if ($this->configModel->save($values)) { + $this->languageModel->loadCurrentLanguage(); + $this->flash->success(t('Settings saved successfully.')); + } else { + $this->flash->failure(t('Unable to save your settings.')); + } + + $this->response->redirect($this->helper->url->to('ConfigController', $redirect)); } /** @@ -87,11 +71,10 @@ class Config extends Base */ public function application() { - $this->common('application'); - $this->response->html($this->helper->layout->config('config/application', array( - 'languages' => $this->config->getLanguages(), - 'timezones' => $this->config->getTimezones(), + 'mail_transports' => $this->emailClient->getAvailableTransports(), + 'languages' => $this->languageModel->getLanguages(), + 'timezones' => $this->timezoneModel->getTimezones(), 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), 'datetime_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateTimeFormats()), 'time_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getTimeFormats()), @@ -99,6 +82,26 @@ class Config extends Base ))); } + /** + * Display the email settings page + * + * @access public + */ + public function email() + { + $values = $this->configModel->getAll(); + + if (empty($values['mail_transport'])) { + $values['mail_transport'] = MAIL_TRANSPORT; + } + + $this->response->html($this->helper->layout->config('config/email', array( + 'values' => $values, + 'mail_transports' => $this->emailClient->getAvailableTransports(), + 'title' => t('Settings').' > '.t('Email settings'), + ))); + } + /** * Display the project settings page * @@ -106,11 +109,9 @@ class Config extends Base */ public function project() { - $this->common('project'); - $this->response->html($this->helper->layout->config('config/project', array( - 'colors' => $this->color->getList(), - 'default_columns' => implode(', ', $this->board->getDefaultColumns()), + 'colors' => $this->colorModel->getList(), + 'default_columns' => implode(', ', $this->boardModel->getDefaultColumns()), 'title' => t('Settings').' > '.t('Project settings'), ))); } @@ -122,8 +123,6 @@ class Config extends Base */ public function board() { - $this->common('board'); - $this->response->html($this->helper->layout->config('config/board', array( 'title' => t('Settings').' > '.t('Board settings'), ))); @@ -136,8 +135,6 @@ class Config extends Base */ public function calendar() { - $this->common('calendar'); - $this->response->html($this->helper->layout->config('config/calendar', array( 'title' => t('Settings').' > '.t('Calendar settings'), ))); @@ -150,8 +147,6 @@ class Config extends Base */ public function integrations() { - $this->common('integrations'); - $this->response->html($this->helper->layout->config('config/integrations', array( 'title' => t('Settings').' > '.t('Integrations'), ))); @@ -164,8 +159,6 @@ class Config extends Base */ public function webhook() { - $this->common('webhook'); - $this->response->html($this->helper->layout->config('config/webhook', array( 'title' => t('Settings').' > '.t('Webhook settings'), ))); @@ -191,8 +184,8 @@ class Config extends Base public function downloadDb() { $this->checkCSRFParam(); - $this->response->forceDownload('db.sqlite.gz'); - $this->response->binary($this->config->downloadDatabase()); + $this->response->withFileDownload('db.sqlite.gz'); + $this->response->binary($this->configModel->downloadDatabase()); } /** @@ -203,9 +196,9 @@ class Config extends Base public function optimizeDb() { $this->checkCSRFParam(); - $this->config->optimizeDatabase(); + $this->configModel->optimizeDatabase(); $this->flash->success(t('Database optimization done.')); - $this->response->redirect($this->helper->url->to('config', 'index')); + $this->response->redirect($this->helper->url->to('ConfigController', 'index')); } /** @@ -218,9 +211,9 @@ class Config extends Base $type = $this->request->getStringParam('type'); $this->checkCSRFParam(); - $this->config->regenerateToken($type.'_token'); + $this->configModel->regenerateToken($type.'_token'); $this->flash->success(t('Token regenerated.')); - $this->response->redirect($this->helper->url->to('config', $type)); + $this->response->redirect($this->helper->url->to('ConfigController', $type)); } } diff --git a/sources/app/Controller/Currency.php b/sources/app/Controller/CurrencyController.php similarity index 66% rename from sources/app/Controller/Currency.php rename to sources/app/Controller/CurrencyController.php index ecaa983..ad59003 100644 --- a/sources/app/Controller/Currency.php +++ b/sources/app/Controller/CurrencyController.php @@ -3,26 +3,28 @@ namespace Kanboard\Controller; /** - * Currency controller + * Currency Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Currency extends Base +class CurrencyController extends BaseController { /** * Display all currency rates and form * * @access public + * @param array $values + * @param array $errors */ public function index(array $values = array(), array $errors = array()) { $this->response->html($this->helper->layout->config('currency/index', array( - 'config_values' => array('application_currency' => $this->config->get('application_currency')), + 'config_values' => array('application_currency' => $this->configModel->get('application_currency')), 'values' => $values, 'errors' => $errors, - 'rates' => $this->currency->getAll(), - 'currencies' => $this->currency->getCurrencies(), + 'rates' => $this->currencyModel->getAll(), + 'currencies' => $this->currencyModel->getCurrencies(), 'title' => t('Settings').' > '.t('Currency rates'), ))); } @@ -38,15 +40,15 @@ class Currency extends Base list($valid, $errors) = $this->currencyValidator->validateCreation($values); if ($valid) { - if ($this->currency->create($values['currency'], $values['rate'])) { + if ($this->currencyModel->create($values['currency'], $values['rate'])) { $this->flash->success(t('The currency rate have been added successfully.')); - $this->response->redirect($this->helper->url->to('currency', 'index')); + return $this->response->redirect($this->helper->url->to('CurrencyController', 'index')); } else { $this->flash->failure(t('Unable to add this currency rate.')); } } - $this->index($values, $errors); + return $this->index($values, $errors); } /** @@ -58,13 +60,12 @@ class Currency extends Base { $values = $this->request->getValues(); - if ($this->config->save($values)) { - $this->config->reload(); + if ($this->configModel->save($values)) { $this->flash->success(t('Settings saved successfully.')); } else { $this->flash->failure(t('Unable to save your settings.')); } - $this->response->redirect($this->helper->url->to('currency', 'index')); + $this->response->redirect($this->helper->url->to('CurrencyController', 'index')); } } diff --git a/sources/app/Controller/Customfilter.php b/sources/app/Controller/CustomFilterController.php similarity index 66% rename from sources/app/Controller/Customfilter.php rename to sources/app/Controller/CustomFilterController.php index 41da0b1..e5f674c 100644 --- a/sources/app/Controller/Customfilter.php +++ b/sources/app/Controller/CustomFilterController.php @@ -2,20 +2,25 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\AccessForbiddenException; use Kanboard\Core\Security\Role; /** - * Custom Filter management + * Custom Filter Controller * - * @package controller + * @package Kanboard\Controller * @author Timo Litzbarski + * @author Frederic Guillot */ -class Customfilter extends Base +class CustomFilterController extends BaseController { /** * Display list of filters * * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\PageNotFoundException */ public function index(array $values = array(), array $errors = array()) { @@ -25,7 +30,7 @@ class Customfilter extends Base 'values' => $values + array('project_id' => $project['id']), 'errors' => $errors, 'project' => $project, - 'custom_filters' => $this->customFilter->getAll($project['id'], $this->userSession->getId()), + 'custom_filters' => $this->customFilterModel->getAll($project['id'], $this->userSession->getId()), 'title' => t('Custom filters'), ))); } @@ -45,15 +50,15 @@ class Customfilter extends Base list($valid, $errors) = $this->customFilterValidator->validateCreation($values); if ($valid) { - if ($this->customFilter->create($values)) { + if ($this->customFilterModel->create($values) !== false) { $this->flash->success(t('Your custom filter have been created successfully.')); - $this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id']))); + return $this->response->redirect($this->helper->url->to('CustomFilterController', 'index', array('project_id' => $project['id']))); } else { $this->flash->failure(t('Unable to create your custom filter.')); } } - $this->index($values, $errors); + return $this->index($values, $errors); } /** @@ -64,7 +69,7 @@ class Customfilter extends Base public function confirm() { $project = $this->getProject(); - $filter = $this->customFilter->getById($this->request->getIntegerParam('filter_id')); + $filter = $this->customFilterModel->getById($this->request->getIntegerParam('filter_id')); $this->response->html($this->helper->layout->project('custom_filter/remove', array( 'project' => $project, @@ -82,28 +87,32 @@ class Customfilter extends Base { $this->checkCSRFParam(); $project = $this->getProject(); - $filter = $this->customFilter->getById($this->request->getIntegerParam('filter_id')); + $filter = $this->customFilterModel->getById($this->request->getIntegerParam('filter_id')); $this->checkPermission($project, $filter); - if ($this->customFilter->remove($filter['id'])) { + if ($this->customFilterModel->remove($filter['id'])) { $this->flash->success(t('Custom filter removed successfully.')); } else { $this->flash->failure(t('Unable to remove this custom filter.')); } - $this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('CustomFilterController', 'index', array('project_id' => $project['id']))); } /** * Edit a custom filter (display the form) * * @access public + * @param array $values + * @param array $errors + * @throws AccessForbiddenException + * @throws \Kanboard\Core\Controller\PageNotFoundException */ public function edit(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $filter = $this->customFilter->getById($this->request->getIntegerParam('filter_id')); + $filter = $this->customFilterModel->getById($this->request->getIntegerParam('filter_id')); $this->checkPermission($project, $filter); @@ -124,7 +133,7 @@ class Customfilter extends Base public function update() { $project = $this->getProject(); - $filter = $this->customFilter->getById($this->request->getIntegerParam('filter_id')); + $filter = $this->customFilterModel->getById($this->request->getIntegerParam('filter_id')); $this->checkPermission($project, $filter); @@ -141,23 +150,23 @@ class Customfilter extends Base list($valid, $errors) = $this->customFilterValidator->validateModification($values); if ($valid) { - if ($this->customFilter->update($values)) { + if ($this->customFilterModel->update($values)) { $this->flash->success(t('Your custom filter have been updated successfully.')); - $this->response->redirect($this->helper->url->to('customfilter', 'index', array('project_id' => $project['id']))); + return $this->response->redirect($this->helper->url->to('CustomFilterController', 'index', array('project_id' => $project['id']))); } else { $this->flash->failure(t('Unable to update custom filter.')); } } - $this->edit($values, $errors); + return $this->edit($values, $errors); } private function checkPermission(array $project, array $filter) { $user_id = $this->userSession->getId(); - if ($filter['user_id'] != $user_id && ($this->projectUserRole->getUserRole($project['id'], $user_id) === Role::PROJECT_MANAGER || ! $this->userSession->isAdmin())) { - $this->forbidden(); + if ($filter['user_id'] != $user_id && ($this->projectUserRoleModel->getUserRole($project['id'], $user_id) === Role::PROJECT_MANAGER || ! $this->userSession->isAdmin())) { + throw new AccessForbiddenException(); } } } diff --git a/sources/app/Controller/App.php b/sources/app/Controller/DashboardController.php similarity index 74% rename from sources/app/Controller/App.php rename to sources/app/Controller/DashboardController.php index df1d3c9..4487454 100644 --- a/sources/app/Controller/App.php +++ b/sources/app/Controller/DashboardController.php @@ -2,16 +2,16 @@ namespace Kanboard\Controller; -use Kanboard\Model\Project as ProjectModel; -use Kanboard\Model\Subtask as SubtaskModel; +use Kanboard\Model\ProjectModel; +use Kanboard\Model\SubtaskModel; /** - * Application controller + * Dashboard Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class App extends Base +class DashboardController extends BaseController { /** * Get project pagination @@ -25,10 +25,10 @@ class App extends Base private function getProjectPaginator($user_id, $action, $max) { return $this->paginator - ->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id)) + ->setUrl('DashboardController', $action, array('pagination' => 'projects', 'user_id' => $user_id)) ->setMax($max) ->setOrder(ProjectModel::TABLE.'.name') - ->setQuery($this->project->getQueryColumnStats($this->projectPermission->getActiveProjectIds($user_id))) + ->setQuery($this->projectModel->getQueryColumnStats($this->projectPermissionModel->getActiveProjectIds($user_id))) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects'); } @@ -44,10 +44,10 @@ class App extends Base private function getTaskPaginator($user_id, $action, $max) { return $this->paginator - ->setUrl('app', $action, array('pagination' => 'tasks', 'user_id' => $user_id)) + ->setUrl('DashboardController', $action, array('pagination' => 'tasks', 'user_id' => $user_id)) ->setMax($max) ->setOrder('tasks.id') - ->setQuery($this->taskFinder->getUserQuery($user_id)) + ->setQuery($this->taskFinderModel->getUserQuery($user_id)) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks'); } @@ -63,37 +63,27 @@ class App extends Base private function getSubtaskPaginator($user_id, $action, $max) { return $this->paginator - ->setUrl('app', $action, array('pagination' => 'subtasks', 'user_id' => $user_id)) + ->setUrl('DashboardController', $action, array('pagination' => 'subtasks', 'user_id' => $user_id)) ->setMax($max) ->setOrder('tasks.id') - ->setQuery($this->subtask->getUserQuery($user_id, array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))) + ->setQuery($this->subtaskModel->getUserQuery($user_id, array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); } - /** - * Check if the user is connected - * - * @access public - */ - public function status() - { - $this->response->text('OK'); - } - /** * Dashboard overview * * @access public */ - public function index() + public function show() { $user = $this->getUser(); - $this->response->html($this->helper->layout->dashboard('app/overview', array( + $this->response->html($this->helper->layout->dashboard('dashboard/show', array( 'title' => t('Dashboard'), - 'project_paginator' => $this->getProjectPaginator($user['id'], 'index', 10), - 'task_paginator' => $this->getTaskPaginator($user['id'], 'index', 10), - 'subtask_paginator' => $this->getSubtaskPaginator($user['id'], 'index', 10), + 'project_paginator' => $this->getProjectPaginator($user['id'], 'show', 10), + 'task_paginator' => $this->getTaskPaginator($user['id'], 'show', 10), + 'subtask_paginator' => $this->getSubtaskPaginator($user['id'], 'show', 10), 'user' => $user, ))); } @@ -107,7 +97,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->helper->layout->dashboard('app/tasks', array( + $this->response->html($this->helper->layout->dashboard('dashboard/tasks', array( 'title' => t('My tasks'), 'paginator' => $this->getTaskPaginator($user['id'], 'tasks', 50), 'user' => $user, @@ -123,7 +113,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->helper->layout->dashboard('app/subtasks', array( + $this->response->html($this->helper->layout->dashboard('dashboard/subtasks', array( 'title' => t('My subtasks'), 'paginator' => $this->getSubtaskPaginator($user['id'], 'subtasks', 50), 'user' => $user, @@ -139,7 +129,7 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->helper->layout->dashboard('app/projects', array( + $this->response->html($this->helper->layout->dashboard('dashboard/projects', array( 'title' => t('My projects'), 'paginator' => $this->getProjectPaginator($user['id'], 'projects', 25), 'user' => $user, @@ -155,9 +145,9 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->helper->layout->dashboard('app/activity', array( + $this->response->html($this->helper->layout->dashboard('dashboard/activity', array( 'title' => t('My activity stream'), - 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100), + 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermissionModel->getActiveProjectIds($user['id']), 100), 'user' => $user, ))); } @@ -169,7 +159,7 @@ class App extends Base */ public function calendar() { - $this->response->html($this->helper->layout->dashboard('app/calendar', array( + $this->response->html($this->helper->layout->dashboard('dashboard/calendar', array( 'title' => t('My calendar'), 'user' => $this->getUser(), ))); @@ -184,9 +174,9 @@ class App extends Base { $user = $this->getUser(); - $this->response->html($this->helper->layout->dashboard('app/notifications', array( + $this->response->html($this->helper->layout->dashboard('dashboard/notifications', array( 'title' => t('My notifications'), - 'notifications' => $this->userUnreadNotification->getAll($user['id']), + 'notifications' => $this->userUnreadNotificationModel->getAll($user['id']), 'user' => $user, ))); } diff --git a/sources/app/Controller/Doc.php b/sources/app/Controller/Doc.php deleted file mode 100644 index 00b9e58..0000000 --- a/sources/app/Controller/Doc.php +++ /dev/null @@ -1,92 +0,0 @@ -request->getStringParam('file', 'index'); - - if (!preg_match('/^[a-z0-9\-]+/', $page)) { - $page = 'index'; - } - - if ($this->config->getCurrentLanguage() === 'fr_FR') { - $filename = __DIR__.'/../../doc/fr/' . $page . '.markdown'; - } else { - $filename = __DIR__ . '/../../doc/' . $page . '.markdown'; - } - - if (!file_exists($filename)) { - $filename = __DIR__.'/../../doc/index.markdown'; - } - - $this->response->html($this->helper->layout->app('doc/show', $this->render($filename))); - } - - /** - * Display keyboard shortcut - */ - public function shortcuts() - { - $this->response->html($this->template->render('config/keyboard_shortcuts')); - } - - /** - * Prepare Markdown file - * - * @access private - * @param string $filename - * @return array - */ - private function render($filename) - { - $data = file_get_contents($filename); - $content = preg_replace_callback('/\((.*.markdown)\)/', array($this, 'replaceMarkdownUrl'), $data); - $content = preg_replace_callback('/\((screenshots.*\.png)\)/', array($this, 'replaceImageUrl'), $content); - - list($title, ) = explode("\n", $data, 2); - - return array( - 'content' => Parsedown::instance()->text($content), - 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title, - ); - } - - /** - * Regex callback to replace Markdown links - * - * @access public - * @param array $matches - * @return string - */ - public function replaceMarkdownUrl(array $matches) - { - return '('.$this->helper->url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')'; - } - - /** - * Regex callback to replace image links - * - * @access public - * @param array $matches - * @return string - */ - public function replaceImageUrl(array $matches) - { - if ($this->config->getCurrentLanguage() === 'fr_FR') { - return '('.$this->helper->url->base().'doc/fr/'.$matches[1].')'; - } - - return '('.$this->helper->url->base().'doc/'.$matches[1].')'; - } -} diff --git a/sources/app/Controller/DocumentationController.php b/sources/app/Controller/DocumentationController.php new file mode 100644 index 0000000..0d02ebd --- /dev/null +++ b/sources/app/Controller/DocumentationController.php @@ -0,0 +1,136 @@ +request->getStringParam('file', 'index'); + + if (!preg_match('/^[a-z0-9\-]+/', $page)) { + $page = 'index'; + } + + $filename = $this->getPageFilename($page); + $this->response->html($this->helper->layout->app('doc/show', $this->render($filename))); + } + + /** + * Display keyboard shortcut + */ + public function shortcuts() + { + $this->response->html($this->template->render('config/keyboard_shortcuts')); + } + + /** + * Prepare Markdown file + * + * @access private + * @param string $filename + * @return array + */ + private function render($filename) + { + $data = file_get_contents($filename); + $content = preg_replace_callback('/\((.*.markdown)\)/', array($this, 'replaceMarkdownUrl'), $data); + $content = preg_replace_callback('/\((screenshots.*\.png)\)/', array($this, 'replaceImageUrl'), $content); + + list($title, ) = explode("\n", $data, 2); + + return array( + 'content' => Parsedown::instance()->text($content), + 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title, + ); + } + + /** + * Regex callback to replace Markdown links + * + * @access public + * @param array $matches + * @return string + */ + public function replaceMarkdownUrl(array $matches) + { + return '('.$this->helper->url->to('DocumentationController', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')'; + } + + /** + * Regex callback to replace image links + * + * @access public + * @param array $matches + * @return string + */ + public function replaceImageUrl(array $matches) + { + return '('.$this->getFileBaseUrl($matches[1]).')'; + } + + /** + * Get Markdown file according to the current language + * + * @access private + * @param string $page + * @return string + */ + private function getPageFilename($page) + { + return $this->getFileLocation($page . '.markdown') ?: + implode(DIRECTORY_SEPARATOR, array(ROOT_DIR, 'doc', 'index.markdown')); + } + + /** + * Get base URL for Markdown links + * + * @access private + * @param string $filename + * @return string + */ + private function getFileBaseUrl($filename) + { + $language = $this->languageModel->getCurrentLanguage(); + $path = $this->getFileLocation($filename); + + if (strpos($path, $language) !== false) { + $url = implode('/', array('doc', $language, $filename)); + } else { + $url = implode('/', array('doc', $filename)); + } + + return $this->helper->url->base().$url; + } + + /** + * Get file location according to the current language + * + * @access private + * @param string $filename + * @return string + */ + private function getFileLocation($filename) + { + $files = array( + implode(DIRECTORY_SEPARATOR, array(ROOT_DIR, 'doc', $this->languageModel->getCurrentLanguage(), $filename)), + implode(DIRECTORY_SEPARATOR, array(ROOT_DIR, 'doc', $filename)), + ); + + foreach ($files as $filename) { + if (file_exists($filename)) { + return $filename; + } + } + + return ''; + } +} diff --git a/sources/app/Controller/Export.php b/sources/app/Controller/ExportController.php similarity index 51% rename from sources/app/Controller/Export.php rename to sources/app/Controller/ExportController.php index c2ff652..27046c7 100644 --- a/sources/app/Controller/Export.php +++ b/sources/app/Controller/ExportController.php @@ -3,17 +3,23 @@ namespace Kanboard\Controller; /** - * Export controller + * Export Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Export extends Base +class ExportController extends BaseController { /** * Common export method * * @access private + * @param string $model + * @param string $method + * @param string $filename + * @param string $action + * @param string $page_title + * @throws \Kanboard\Core\Controller\PageNotFoundException */ private function common($model, $method, $filename, $action, $page_title) { @@ -23,24 +29,25 @@ class Export extends Base if ($from && $to) { $data = $this->$model->$method($project['id'], $from, $to); - $this->response->forceDownload($filename.'.csv'); + $this->response->withFileDownload($filename.'.csv'); $this->response->csv($data); - } + } else { - $this->response->html($this->helper->layout->project('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($this->dateParser->getDateFormats()), - 'project' => $project, - 'title' => $page_title, - ), 'export/sidebar')); + $this->response->html($this->helper->layout->project('export/'.$action, array( + 'values' => array( + 'controller' => 'ExportController', + 'action' => $action, + 'project_id' => $project['id'], + 'from' => $from, + 'to' => $to, + ), + 'errors' => array(), + 'date_format' => $this->configModel->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats($this->dateParser->getDateFormats()), + 'project' => $project, + 'title' => $page_title, + ), 'export/sidebar')); + } } /** @@ -70,7 +77,7 @@ class Export extends Base */ public function summary() { - $this->common('projectDailyColumnStats', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); + $this->common('projectDailyColumnStatsModel', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); } /** diff --git a/sources/app/Controller/Feed.php b/sources/app/Controller/FeedController.php similarity index 56% rename from sources/app/Controller/Feed.php rename to sources/app/Controller/FeedController.php index 8457c38..cf2b108 100644 --- a/sources/app/Controller/Feed.php +++ b/sources/app/Controller/FeedController.php @@ -2,13 +2,15 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\AccessForbiddenException; + /** * Atom/RSS Feed controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Feed extends Base +class FeedController extends BaseController { /** * RSS feed for a user @@ -18,15 +20,15 @@ class Feed extends Base public function user() { $token = $this->request->getStringParam('token'); - $user = $this->user->getByToken($token); + $user = $this->userModel->getByToken($token); // Token verification if (empty($user)) { - $this->forbidden(true); + throw AccessForbiddenException::getInstance()->withoutLayout(); } $this->response->xml($this->template->render('feed/user', array( - 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])), + 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermissionModel->getActiveProjectIds($user['id'])), 'user' => $user, ))); } @@ -39,15 +41,14 @@ class Feed extends Base public function project() { $token = $this->request->getStringParam('token'); - $project = $this->project->getByToken($token); + $project = $this->projectModel->getByToken($token); - // Token verification if (empty($project)) { - $this->forbidden(true); + throw AccessForbiddenException::getInstance()->withoutLayout(); } $this->response->xml($this->template->render('feed/project', array( - 'events' => $this->projectActivity->getProject($project['id']), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id']), 'project' => $project, ))); } diff --git a/sources/app/Controller/FileViewer.php b/sources/app/Controller/FileViewerController.php similarity index 61% rename from sources/app/Controller/FileViewer.php rename to sources/app/Controller/FileViewerController.php index 3be4ea1..518f5b0 100644 --- a/sources/app/Controller/FileViewer.php +++ b/sources/app/Controller/FileViewerController.php @@ -7,10 +7,10 @@ use Kanboard\Core\ObjectStorage\ObjectStorageException; /** * File Viewer Controller * - * @package controller + * @package Kanbaord\Controller * @author Frederic Guillot */ -class FileViewer extends Base +class FileViewerController extends BaseController { /** * Get file content from object storage @@ -24,11 +24,9 @@ class FileViewer extends Base $content = ''; try { - if ($file['is_image'] == 0) { $content = $this->objectStorage->get($file['path']); } - } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); } @@ -47,7 +45,7 @@ class FileViewer extends Base $type = $this->helper->file->getPreviewType($file['name']); $params = array('file_id' => $file['id'], 'project_id' => $this->request->getIntegerParam('project_id')); - if ($file['model'] === 'taskFile') { + if ($file['model'] === 'taskFileModel') { $params['task_id'] = $file['task_id']; } @@ -68,17 +66,19 @@ class FileViewer extends Base { $file = $this->getFile(); $etag = md5($file['path']); - $this->response->contentType($this->helper->file->getImageMimeType($file['name'])); - $this->response->cache(5 * 86400, $etag); + $this->response->withContentType($this->helper->file->getImageMimeType($file['name'])); + $this->response->withCache(5 * 86400, $etag); if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { - return $this->response->status(304); - } + $this->response->status(304); + } else { - try { - $this->objectStorage->output($file['path']); - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); + try { + $this->response->send(); + $this->objectStorage->output($file['path']); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } } } @@ -94,23 +94,26 @@ class FileViewer extends Base $filename = $this->$model->getThumbnailPath($file['path']); $etag = md5($filename); - $this->response->cache(5 * 86400, $etag); - $this->response->contentType('image/jpeg'); + $this->response->withCache(5 * 86400, $etag); + $this->response->withContentType('image/jpeg'); if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { - return $this->response->status(304); - } + $this->response->status(304); + } else { - try { + $this->response->send(); - $this->objectStorage->output($filename); - } catch (ObjectStorageException $e) { - $this->logger->error($e->getMessage()); + try { - // Try to generate thumbnail on the fly for images uploaded before Kanboard < 1.0.19 - $data = $this->objectStorage->get($file['path']); - $this->$model->generateThumbnailFromData($file['path'], $data); - $this->objectStorage->output($this->$model->getThumbnailPath($file['path'])); + $this->objectStorage->output($filename); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + + // Try to generate thumbnail on the fly for images uploaded before Kanboard < 1.0.19 + $data = $this->objectStorage->get($file['path']); + $this->$model->generateThumbnailFromData($file['path'], $data); + $this->objectStorage->output($this->$model->getThumbnailPath($file['path'])); + } } } @@ -123,7 +126,8 @@ class FileViewer extends Base { try { $file = $this->getFile(); - $this->response->forceDownload($file['name']); + $this->response->withFileDownload($file['name']); + $this->response->send(); $this->objectStorage->output($file['path']); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); diff --git a/sources/app/Controller/Gantt.php b/sources/app/Controller/Gantt.php deleted file mode 100644 index 02ee946..0000000 --- a/sources/app/Controller/Gantt.php +++ /dev/null @@ -1,153 +0,0 @@ -userSession->isAdmin()) { - $project_ids = $this->project->getAllIds(); - } else { - $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); - } - - $this->response->html($this->helper->layout->app('gantt/projects', array( - 'projects' => $this->projectGanttFormatter->filter($project_ids)->format(), - 'title' => t('Gantt chart for all projects'), - ))); - } - - /** - * Save new project start date and end date - */ - public function saveProjectDate() - { - $values = $this->request->getJson(); - - $result = $this->project->update(array( - 'id' => $values['id'], - 'start_date' => $this->dateParser->getIsoDate(strtotime($values['start'])), - 'end_date' => $this->dateParser->getIsoDate(strtotime($values['end'])), - )); - - if (! $result) { - $this->response->json(array('message' => 'Unable to save project'), 400); - } - - $this->response->json(array('message' => 'OK'), 201); - } - - /** - * Show Gantt chart for one project - */ - public function project() - { - $project = $this->getProject(); - $search = $this->helper->projectHeader->getSearchQuery($project); - $filter = $this->taskFilterGanttFormatter->search($search)->filterByProject($project['id']); - $sorting = $this->request->getStringParam('sorting', 'board'); - - if ($sorting === 'date') { - $filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation'); - } else { - $filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position'); - } - - $this->response->html($this->helper->layout->app('gantt/project', array( - 'project' => $project, - 'title' => $project['name'], - 'description' => $this->helper->projectHeader->getDescription($project), - 'sorting' => $sorting, - 'tasks' => $filter->format(), - ))); - } - - /** - * Save new task start date and due date - */ - public function saveTaskDate() - { - $this->getProject(); - $values = $this->request->getJson(); - - $result = $this->taskModification->update(array( - 'id' => $values['id'], - 'date_started' => strtotime($values['start']), - 'date_due' => strtotime($values['end']), - )); - - if (! $result) { - $this->response->json(array('message' => 'Unable to save task'), 400); - } - - $this->response->json(array('message' => 'OK'), 201); - } - - /** - * Simplified form to create a new task - * - * @access public - */ - public function task(array $values = array(), array $errors = array()) - { - $project = $this->getProject(); - - $values = $values + array( - 'project_id' => $project['id'], - 'column_id' => $this->column->getFirstColumnId($project['id']), - 'position' => 1 - ); - - $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); - $values = $this->hook->merge('controller:gantt:task:form:default', $values, array('default_values' => $values)); - - $this->response->html($this->template->render('gantt/task_creation', array( - 'project' => $project, - 'errors' => $errors, - 'values' => $values, - 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($project['id']), - 'swimlanes_list' => $this->swimlane->getList($project['id'], false, true), - 'title' => $project['name'].' > '.t('New task') - ))); - } - - /** - * Validate and save a new task - * - * @access public - */ - public function saveTask() - { - $project = $this->getProject(); - $values = $this->request->getValues(); - - list($valid, $errors) = $this->taskValidator->validateCreation($values); - - if ($valid) { - $task_id = $this->taskCreation->create($values); - - if ($task_id !== false) { - $this->flash->success(t('Task created successfully.')); - $this->response->redirect($this->helper->url->to('gantt', 'project', array('project_id' => $project['id']))); - } else { - $this->flash->failure(t('Unable to create your task.')); - } - } - - $this->task($values, $errors); - } -} diff --git a/sources/app/Controller/Group.php b/sources/app/Controller/Group.php deleted file mode 100644 index fa47f42..0000000 --- a/sources/app/Controller/Group.php +++ /dev/null @@ -1,250 +0,0 @@ -paginator - ->setUrl('group', 'index') - ->setMax(30) - ->setOrder('name') - ->setQuery($this->group->getQuery()) - ->calculate(); - - $this->response->html($this->helper->layout->app('group/index', array( - 'title' => t('Groups').' ('.$paginator->getTotal().')', - 'paginator' => $paginator, - ))); - } - - /** - * List all users - * - * @access public - */ - public function users() - { - $group_id = $this->request->getIntegerParam('group_id'); - $group = $this->group->getById($group_id); - - $paginator = $this->paginator - ->setUrl('group', 'users', array('group_id' => $group_id)) - ->setMax(30) - ->setOrder('username') - ->setQuery($this->groupMember->getQuery($group_id)) - ->calculate(); - - $this->response->html($this->helper->layout->app('group/users', array( - 'title' => t('Members of %s', $group['name']).' ('.$paginator->getTotal().')', - 'paginator' => $paginator, - 'group' => $group, - ))); - } - - /** - * Display a form to create a new group - * - * @access public - */ - public function create(array $values = array(), array $errors = array()) - { - $this->response->html($this->helper->layout->app('group/create', array( - 'errors' => $errors, - 'values' => $values, - 'title' => t('New group') - ))); - } - - /** - * Validate and save a new group - * - * @access public - */ - public function save() - { - $values = $this->request->getValues(); - list($valid, $errors) = $this->groupValidator->validateCreation($values); - - if ($valid) { - if ($this->group->create($values['name']) !== false) { - $this->flash->success(t('Group created successfully.')); - $this->response->redirect($this->helper->url->to('group', 'index')); - } else { - $this->flash->failure(t('Unable to create your group.')); - } - } - - $this->create($values, $errors); - } - - /** - * Display a form to update a group - * - * @access public - */ - public function edit(array $values = array(), array $errors = array()) - { - if (empty($values)) { - $values = $this->group->getById($this->request->getIntegerParam('group_id')); - } - - $this->response->html($this->helper->layout->app('group/edit', array( - 'errors' => $errors, - 'values' => $values, - 'title' => t('Edit group') - ))); - } - - /** - * Validate and save a group - * - * @access public - */ - public function update() - { - $values = $this->request->getValues(); - list($valid, $errors) = $this->groupValidator->validateModification($values); - - if ($valid) { - if ($this->group->update($values) !== false) { - $this->flash->success(t('Group updated successfully.')); - $this->response->redirect($this->helper->url->to('group', 'index')); - } else { - $this->flash->failure(t('Unable to update your group.')); - } - } - - $this->edit($values, $errors); - } - - /** - * Form to associate a user to a group - * - * @access public - */ - public function associate(array $values = array(), array $errors = array()) - { - $group_id = $this->request->getIntegerParam('group_id'); - $group = $this->group->getbyId($group_id); - - if (empty($values)) { - $values['group_id'] = $group_id; - } - - $this->response->html($this->helper->layout->app('group/associate', array( - 'users' => $this->user->prepareList($this->groupMember->getNotMembers($group_id)), - 'group' => $group, - 'errors' => $errors, - 'values' => $values, - 'title' => t('Add group member to "%s"', $group['name']), - ))); - } - - /** - * Add user to a group - * - * @access public - */ - public function addUser() - { - $values = $this->request->getValues(); - - if (isset($values['group_id']) && isset($values['user_id'])) { - if ($this->groupMember->addUser($values['group_id'], $values['user_id'])) { - $this->flash->success(t('Group member added successfully.')); - $this->response->redirect($this->helper->url->to('group', 'users', array('group_id' => $values['group_id']))); - } else { - $this->flash->failure(t('Unable to add group member.')); - } - } - - $this->associate($values); - } - - /** - * Confirmation dialog to remove a user from a group - * - * @access public - */ - public function dissociate() - { - $group_id = $this->request->getIntegerParam('group_id'); - $user_id = $this->request->getIntegerParam('user_id'); - $group = $this->group->getById($group_id); - $user = $this->user->getById($user_id); - - $this->response->html($this->helper->layout->app('group/dissociate', array( - 'group' => $group, - 'user' => $user, - 'title' => t('Remove user from group "%s"', $group['name']), - ))); - } - - /** - * Remove a user from a group - * - * @access public - */ - public function removeUser() - { - $this->checkCSRFParam(); - $group_id = $this->request->getIntegerParam('group_id'); - $user_id = $this->request->getIntegerParam('user_id'); - - if ($this->groupMember->removeUser($group_id, $user_id)) { - $this->flash->success(t('User removed successfully from this group.')); - } else { - $this->flash->failure(t('Unable to remove this user from the group.')); - } - - $this->response->redirect($this->helper->url->to('group', 'users', array('group_id' => $group_id))); - } - - /** - * Confirmation dialog to remove a group - * - * @access public - */ - public function confirm() - { - $group_id = $this->request->getIntegerParam('group_id'); - $group = $this->group->getById($group_id); - - $this->response->html($this->helper->layout->app('group/remove', array( - 'group' => $group, - 'title' => t('Remove group'), - ))); - } - - /** - * Remove a group - * - * @access public - */ - public function remove() - { - $this->checkCSRFParam(); - $group_id = $this->request->getIntegerParam('group_id'); - - if ($this->group->remove($group_id)) { - $this->flash->success(t('Group removed successfully.')); - } else { - $this->flash->failure(t('Unable to remove this group.')); - } - - $this->response->redirect($this->helper->url->to('group', 'index')); - } -} diff --git a/sources/app/Controller/GroupAjaxController.php b/sources/app/Controller/GroupAjaxController.php new file mode 100644 index 0000000..496e9ef --- /dev/null +++ b/sources/app/Controller/GroupAjaxController.php @@ -0,0 +1,26 @@ +request->getStringParam('term'); + $formatter = new GroupAutoCompleteFormatter($this->groupManager->find($search)); + $this->response->json($formatter->format()); + } +} diff --git a/sources/app/Controller/GroupCreationController.php b/sources/app/Controller/GroupCreationController.php new file mode 100644 index 0000000..b297b19 --- /dev/null +++ b/sources/app/Controller/GroupCreationController.php @@ -0,0 +1,49 @@ +response->html($this->template->render('group_creation/show', array( + 'errors' => $errors, + 'values' => $values, + ))); + } + + /** + * Validate and save a new group + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->groupValidator->validateCreation($values); + + if ($valid) { + if ($this->groupModel->create($values['name']) !== false) { + $this->flash->success(t('Group created successfully.')); + return $this->response->redirect($this->helper->url->to('GroupListController', 'index'), true); + } else { + $this->flash->failure(t('Unable to create your group.')); + } + } + + return $this->show($values, $errors); + } +} diff --git a/sources/app/Controller/GroupHelper.php b/sources/app/Controller/GroupHelper.php deleted file mode 100644 index 34f522a..0000000 --- a/sources/app/Controller/GroupHelper.php +++ /dev/null @@ -1,24 +0,0 @@ -request->getStringParam('term'); - $groups = $this->groupManager->find($search); - $this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format()); - } -} diff --git a/sources/app/Controller/GroupListController.php b/sources/app/Controller/GroupListController.php new file mode 100644 index 0000000..4486bbf --- /dev/null +++ b/sources/app/Controller/GroupListController.php @@ -0,0 +1,173 @@ +paginator + ->setUrl('GroupListController', 'index') + ->setMax(30) + ->setOrder('name') + ->setQuery($this->groupModel->getQuery()) + ->calculate(); + + $this->response->html($this->helper->layout->app('group/index', array( + 'title' => t('Groups').' ('.$paginator->getTotal().')', + 'paginator' => $paginator, + ))); + } + + /** + * List all users + * + * @access public + */ + public function users() + { + $group_id = $this->request->getIntegerParam('group_id'); + $group = $this->groupModel->getById($group_id); + + $paginator = $this->paginator + ->setUrl('GroupListController', 'users', array('group_id' => $group_id)) + ->setMax(30) + ->setOrder('username') + ->setQuery($this->groupMemberModel->getQuery($group_id)) + ->calculate(); + + $this->response->html($this->helper->layout->app('group/users', array( + 'title' => t('Members of %s', $group['name']).' ('.$paginator->getTotal().')', + 'paginator' => $paginator, + 'group' => $group, + ))); + } + + /** + * Form to associate a user to a group + * + * @access public + * @param array $values + * @param array $errors + */ + public function associate(array $values = array(), array $errors = array()) + { + $group_id = $this->request->getIntegerParam('group_id'); + $group = $this->groupModel->getById($group_id); + + if (empty($values)) { + $values['group_id'] = $group_id; + } + + $this->response->html($this->template->render('group/associate', array( + 'users' => $this->userModel->prepareList($this->groupMemberModel->getNotMembers($group_id)), + 'group' => $group, + 'errors' => $errors, + 'values' => $values, + ))); + } + + /** + * Add user to a group + * + * @access public + */ + public function addUser() + { + $values = $this->request->getValues(); + + if (isset($values['group_id']) && isset($values['user_id'])) { + if ($this->groupMemberModel->addUser($values['group_id'], $values['user_id'])) { + $this->flash->success(t('Group member added successfully.')); + return $this->response->redirect($this->helper->url->to('GroupListController', 'users', array('group_id' => $values['group_id'])), true); + } else { + $this->flash->failure(t('Unable to add group member.')); + } + } + + return $this->associate($values); + } + + /** + * Confirmation dialog to remove a user from a group + * + * @access public + */ + public function dissociate() + { + $group_id = $this->request->getIntegerParam('group_id'); + $user_id = $this->request->getIntegerParam('user_id'); + $group = $this->groupModel->getById($group_id); + $user = $this->userModel->getById($user_id); + + $this->response->html($this->template->render('group/dissociate', array( + 'group' => $group, + 'user' => $user, + ))); + } + + /** + * Remove a user from a group + * + * @access public + */ + public function removeUser() + { + $this->checkCSRFParam(); + $group_id = $this->request->getIntegerParam('group_id'); + $user_id = $this->request->getIntegerParam('user_id'); + + if ($this->groupMemberModel->removeUser($group_id, $user_id)) { + $this->flash->success(t('User removed successfully from this group.')); + } else { + $this->flash->failure(t('Unable to remove this user from the group.')); + } + + $this->response->redirect($this->helper->url->to('GroupListController', 'users', array('group_id' => $group_id)), true); + } + + /** + * Confirmation dialog to remove a group + * + * @access public + */ + public function confirm() + { + $group_id = $this->request->getIntegerParam('group_id'); + $group = $this->groupModel->getById($group_id); + + $this->response->html($this->template->render('group/remove', array( + 'group' => $group, + ))); + } + + /** + * Remove a group + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $group_id = $this->request->getIntegerParam('group_id'); + + if ($this->groupModel->remove($group_id)) { + $this->flash->success(t('Group removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this group.')); + } + + $this->response->redirect($this->helper->url->to('GroupListController', 'index'), true); + } +} diff --git a/sources/app/Controller/GroupModificationController.php b/sources/app/Controller/GroupModificationController.php new file mode 100644 index 0000000..bd181b7 --- /dev/null +++ b/sources/app/Controller/GroupModificationController.php @@ -0,0 +1,53 @@ +groupModel->getById($this->request->getIntegerParam('group_id')); + } + + $this->response->html($this->template->render('group_modification/show', array( + 'errors' => $errors, + 'values' => $values, + ))); + } + + /** + * Validate and save a group + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->groupValidator->validateModification($values); + + if ($valid) { + if ($this->groupModel->update($values) !== false) { + $this->flash->success(t('Group updated successfully.')); + return $this->response->redirect($this->helper->url->to('GroupListController', 'index'), true); + } else { + $this->flash->failure(t('Unable to update your group.')); + } + } + + return $this->show($values, $errors); + } +} diff --git a/sources/app/Controller/ICalendarController.php b/sources/app/Controller/ICalendarController.php new file mode 100644 index 0000000..e354c6f --- /dev/null +++ b/sources/app/Controller/ICalendarController.php @@ -0,0 +1,101 @@ +request->getStringParam('token'); + $user = $this->userModel->getByToken($token); + + // Token verification + if (empty($user)) { + throw AccessForbiddenException::getInstance()->withoutLayout(); + } + + // Common filter + $queryBuilder = new QueryBuilder(); + $queryBuilder + ->withQuery($this->taskFinderModel->getICalQuery()) + ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN)) + ->withFilter(new TaskAssigneeFilter($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($queryBuilder, $calendar); + } + + /** + * Get project iCalendar + * + * @access public + */ + public function project() + { + $token = $this->request->getStringParam('token'); + $project = $this->projectModel->getByToken($token); + + // Token verification + if (empty($project)) { + throw AccessForbiddenException::getInstance()->withoutLayout(); + } + + // Common filter + $queryBuilder = new QueryBuilder(); + $queryBuilder + ->withQuery($this->taskFinderModel->getICalQuery()) + ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN)) + ->withFilter(new TaskProjectFilter($project['id'])); + + // Calendar properties + $calendar = new iCalendar('Kanboard'); + $calendar->setName($project['name']); + $calendar->setDescription($project['name']); + $calendar->setPublishedTTL('PT1H'); + + $this->renderCalendar($queryBuilder, $calendar); + } + + /** + * Common method to render iCal events + * + * @access private + * @param QueryBuilder $queryBuilder + * @param iCalendar $calendar + */ + private function renderCalendar(QueryBuilder $queryBuilder, iCalendar $calendar) + { + $start = $this->request->getStringParam('start', strtotime('-2 month')); + $end = $this->request->getStringParam('end', strtotime('+6 months')); + + $this->helper->ical->addTaskDateDueEvents($queryBuilder, $calendar, $start, $end); + + $formatter = new TaskICalFormatter($this->container); + $this->response->ical($formatter->setCalendar($calendar)->format()); + } +} diff --git a/sources/app/Controller/Ical.php b/sources/app/Controller/Ical.php deleted file mode 100644 index f1ea6d8..0000000 --- a/sources/app/Controller/Ical.php +++ /dev/null @@ -1,115 +0,0 @@ -request->getStringParam('token'); - $user = $this->user->getByToken($token); - - // Token verification - if (empty($user)) { - $this->forbidden(true); - } - - // Common filter - $filter = $this->taskFilterICalendarFormatter - ->create() - ->filterByStatus(TaskModel::STATUS_OPEN) - ->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->taskFilterICalendarFormatter - ->create() - ->filterByStatus(TaskModel::STATUS_OPEN) - ->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('-2 month')); - $end = $this->request->getStringParam('end', strtotime('+6 months')); - - // Tasks - if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { - $filter - ->copy() - ->filterByCreationDateRange($start, $end) - ->setColumns('date_creation', 'date_completed') - ->setCalendar($calendar) - ->addDateTimeEvents(); - } else { - $filter - ->copy() - ->filterByStartDateRange($start, $end) - ->setColumns('date_started', 'date_completed') - ->setCalendar($calendar) - ->addDateTimeEvents($calendar); - } - - // Tasks with due date - $filter - ->copy() - ->filterByDueDateRange($start, $end) - ->setColumns('date_due') - ->setCalendar($calendar) - ->addFullDayEvents($calendar); - - $this->response->contentType('text/calendar; charset=utf-8'); - echo $filter->setCalendar($calendar)->format(); - } -} diff --git a/sources/app/Controller/Link.php b/sources/app/Controller/LinkController.php similarity index 70% rename from sources/app/Controller/Link.php rename to sources/app/Controller/LinkController.php index ec7ab1a..477b25a 100644 --- a/sources/app/Controller/Link.php +++ b/sources/app/Controller/LinkController.php @@ -2,27 +2,30 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\PageNotFoundException; + /** - * Link controller + * Link Controller * - * @package controller + * @package Kanboard\Controller * @author Olivier Maridat * @author Frederic Guillot */ -class Link extends Base +class LinkController extends BaseController { /** * Get the current link * * @access private * @return array + * @throws PageNotFoundException */ private function getLink() { - $link = $this->link->getById($this->request->getIntegerParam('link_id')); + $link = $this->linkModel->getById($this->request->getIntegerParam('link_id')); if (empty($link)) { - $this->notfound(); + throw new PageNotFoundException(); } return $link; @@ -32,11 +35,13 @@ class Link extends Base * List of links * * @access public + * @param array $values + * @param array $errors */ public function index(array $values = array(), array $errors = array()) { $this->response->html($this->helper->layout->config('link/index', array( - 'links' => $this->link->getMergedList(), + 'links' => $this->linkModel->getMergedList(), 'values' => $values, 'errors' => $errors, 'title' => t('Settings').' > '.t('Task\'s links'), @@ -54,21 +59,24 @@ class Link extends Base list($valid, $errors) = $this->linkValidator->validateCreation($values); if ($valid) { - if ($this->link->create($values['label'], $values['opposite_label']) !== false) { + if ($this->linkModel->create($values['label'], $values['opposite_label']) !== false) { $this->flash->success(t('Link added successfully.')); - $this->response->redirect($this->helper->url->to('link', 'index')); + return $this->response->redirect($this->helper->url->to('LinkController', 'index')); } else { $this->flash->failure(t('Unable to create your link.')); } } - $this->index($values, $errors); + return $this->index($values, $errors); } /** * Edit form * * @access public + * @param array $values + * @param array $errors + * @throws PageNotFoundException */ public function edit(array $values = array(), array $errors = array()) { @@ -78,7 +86,7 @@ class Link extends Base $this->response->html($this->helper->layout->config('link/edit', array( 'values' => $values ?: $link, 'errors' => $errors, - 'labels' => $this->link->getList($link['id']), + 'labels' => $this->linkModel->getList($link['id']), 'link' => $link, 'title' => t('Link modification') ))); @@ -95,15 +103,15 @@ class Link extends Base list($valid, $errors) = $this->linkValidator->validateModification($values); if ($valid) { - if ($this->link->update($values)) { + if ($this->linkModel->update($values)) { $this->flash->success(t('Link updated successfully.')); - $this->response->redirect($this->helper->url->to('link', 'index')); + return $this->response->redirect($this->helper->url->to('LinkController', 'index')); } else { $this->flash->failure(t('Unable to update your link.')); } } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** @@ -131,12 +139,12 @@ class Link extends Base $this->checkCSRFParam(); $link = $this->getLink(); - if ($this->link->remove($link['id'])) { + if ($this->linkModel->remove($link['id'])) { $this->flash->success(t('Link removed successfully.')); } else { $this->flash->failure(t('Unable to remove this link.')); } - $this->response->redirect($this->helper->url->to('link', 'index')); + $this->response->redirect($this->helper->url->to('LinkController', 'index')); } } diff --git a/sources/app/Controller/Oauth.php b/sources/app/Controller/OAuthController.php similarity index 86% rename from sources/app/Controller/Oauth.php rename to sources/app/Controller/OAuthController.php index 12b9114..7663ddc 100644 --- a/sources/app/Controller/Oauth.php +++ b/sources/app/Controller/OAuthController.php @@ -5,12 +5,12 @@ namespace Kanboard\Controller; use Kanboard\Core\Security\OAuthAuthenticationProviderInterface; /** - * OAuth controller + * OAuth Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Oauth extends Base +class OAuthController extends BaseController { /** * Redirect to the provider if no code received @@ -49,7 +49,7 @@ class Oauth extends Base $this->link($provider); } else { $this->flash->failure(t('The OAuth2 state parameter is invalid')); - $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + $this->response->redirect($this->helper->url->to('UserViewController', 'external', array('user_id' => $this->userSession->getId()))); } } else { if ($hasValidState) { @@ -75,7 +75,7 @@ class Oauth extends Base $this->flash->success(t('Your external account is linked to your profile successfully.')); } - $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + $this->response->redirect($this->helper->url->to('UserViewController', 'external', array('user_id' => $this->userSession->getId()))); } /** @@ -94,7 +94,7 @@ class Oauth extends Base $this->flash->failure(t('Unable to unlink your external account.')); } - $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + $this->response->redirect($this->helper->url->to('UserViewController', 'external', array('user_id' => $this->userSession->getId()))); } /** @@ -106,7 +106,7 @@ class Oauth extends Base protected function authenticate($providerName) { if ($this->authenticationManager->oauthAuthentication($providerName)) { - $this->response->redirect($this->helper->url->to('app', 'index')); + $this->response->redirect($this->helper->url->to('DashboardController', 'show')); } else { $this->authenticationFailure(t('External authentication failed')); } diff --git a/sources/app/Controller/PasswordReset.php b/sources/app/Controller/PasswordResetController.php similarity index 62% rename from sources/app/Controller/PasswordReset.php rename to sources/app/Controller/PasswordResetController.php index f6a0eb8..a1780ed 100644 --- a/sources/app/Controller/PasswordReset.php +++ b/sources/app/Controller/PasswordResetController.php @@ -2,16 +2,22 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\AccessForbiddenException; + /** * Password Reset Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class PasswordReset extends Base +class PasswordResetController extends BaseController { /** * Show the form to reset the password + * + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\BaseException */ public function create(array $values = array(), array $errors = array()) { @@ -36,21 +42,25 @@ class PasswordReset extends Base if ($valid) { $this->sendEmail($values['username']); - $this->response->redirect($this->helper->url->to('auth', 'login')); + $this->response->redirect($this->helper->url->to('AuthController', 'login')); + } else { + $this->create($values, $errors); } - - $this->create($values, $errors); } /** * Show the form to set a new password + * + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\BaseException */ public function change(array $values = array(), array $errors = array()) { $this->checkActivation(); $token = $this->request->getStringParam('token'); - $user_id = $this->passwordReset->getUserIdByToken($token); + $user_id = $this->passwordResetModel->getUserIdByToken($token); if ($user_id !== false) { $this->response->html($this->helper->layout->app('password_reset/change', array( @@ -59,9 +69,9 @@ class PasswordReset extends Base 'values' => $values, 'no_layout' => true, ))); + } else { + $this->response->redirect($this->helper->url->to('AuthController', 'login')); } - - $this->response->redirect($this->helper->url->to('auth', 'login')); } /** @@ -76,28 +86,30 @@ class PasswordReset extends Base list($valid, $errors) = $this->passwordResetValidator->validateModification($values); if ($valid) { - $user_id = $this->passwordReset->getUserIdByToken($token); + $user_id = $this->passwordResetModel->getUserIdByToken($token); if ($user_id !== false) { - $this->user->update(array('id' => $user_id, 'password' => $values['password'])); - $this->passwordReset->disable($user_id); + $this->userModel->update(array('id' => $user_id, 'password' => $values['password'])); + $this->passwordResetModel->disable($user_id); } - $this->response->redirect($this->helper->url->to('auth', 'login')); + return $this->response->redirect($this->helper->url->to('AuthController', 'login')); } - $this->change($values, $errors); + return $this->change($values, $errors); } /** * Send the email + * + * @param string $username */ private function sendEmail($username) { - $token = $this->passwordReset->create($username); + $token = $this->passwordResetModel->create($username); if ($token !== false) { - $user = $this->user->getByUsername($username); + $user = $this->userModel->getByUsername($username); $this->emailClient->send( $user['email'], @@ -113,8 +125,8 @@ class PasswordReset extends Base */ private function checkActivation() { - if ($this->config->get('password_reset', 0) == 0) { - $this->response->redirect($this->helper->url->to('auth', 'login')); + if ($this->configModel->get('password_reset', 0) == 0) { + throw AccessForbiddenException::getInstance()->withoutLayout(); } } } diff --git a/sources/app/Controller/PluginController.php b/sources/app/Controller/PluginController.php new file mode 100644 index 0000000..7b9d64d --- /dev/null +++ b/sources/app/Controller/PluginController.php @@ -0,0 +1,126 @@ +response->html($this->helper->layout->plugin('plugin/show', array( + 'plugins' => $this->pluginLoader->getPlugins(), + 'title' => t('Installed Plugins'), + 'is_configured' => Installer::isConfigured(), + ))); + } + + /** + * Display list of available plugins + */ + public function directory() + { + $installedPlugins = array(); + + foreach ($this->pluginLoader->getPlugins() as $plugin) { + $installedPlugins[$plugin->getPluginName()] = $plugin->getPluginVersion(); + } + + $this->response->html($this->helper->layout->plugin('plugin/directory', array( + 'installed_plugins' => $installedPlugins, + 'available_plugins' => Directory::getInstance($this->container)->getAvailablePlugins(), + 'title' => t('Plugin Directory'), + 'is_configured' => Installer::isConfigured(), + ))); + } + + /** + * Install plugin from URL + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + */ + public function install() + { + $this->checkCSRFParam(); + $pluginArchiveUrl = urldecode($this->request->getStringParam('archive_url')); + + try { + $installer = new Installer($this->container); + $installer->install($pluginArchiveUrl); + $this->flash->success(t('Plugin installed successfully.')); + } catch (PluginInstallerException $e) { + $this->flash->failure($e->getMessage()); + } + + $this->response->redirect($this->helper->url->to('PluginController', 'show')); + } + + /** + * Update plugin from URL + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + */ + public function update() + { + $this->checkCSRFParam(); + $pluginArchiveUrl = urldecode($this->request->getStringParam('archive_url')); + + try { + $installer = new Installer($this->container); + $installer->update($pluginArchiveUrl); + $this->flash->success(t('Plugin updated successfully.')); + } catch (PluginInstallerException $e) { + $this->flash->failure($e->getMessage()); + } + + $this->response->redirect($this->helper->url->to('PluginController', 'show')); + } + + /** + * Confirmation before to remove the plugin + */ + public function confirm() + { + $pluginId = $this->request->getStringParam('pluginId'); + $plugins = $this->pluginLoader->getPlugins(); + + $this->response->html($this->template->render('plugin/remove', array( + 'plugin_id' => $pluginId, + 'plugin' => $plugins[$pluginId], + ))); + } + + /** + * Remove a plugin + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + */ + public function uninstall() + { + $this->checkCSRFParam(); + $pluginId = $this->request->getStringParam('pluginId'); + + try { + $installer = new Installer($this->container); + $installer->uninstall($pluginId); + $this->flash->success(t('Plugin removed successfully.')); + } catch (PluginInstallerException $e) { + $this->flash->failure($e->getMessage()); + } + + $this->response->redirect($this->helper->url->to('PluginController', 'show')); + } +} diff --git a/sources/app/Controller/Project.php b/sources/app/Controller/Project.php deleted file mode 100644 index cdfbd94..0000000 --- a/sources/app/Controller/Project.php +++ /dev/null @@ -1,243 +0,0 @@ -userSession->isAdmin()) { - $project_ids = $this->project->getAllIds(); - } else { - $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); - } - - $nb_projects = count($project_ids); - - $paginator = $this->paginator - ->setUrl('project', 'index') - ->setMax(20) - ->setOrder('name') - ->setQuery($this->project->getQueryColumnStats($project_ids)) - ->calculate(); - - $this->response->html($this->helper->layout->app('project/index', array( - 'paginator' => $paginator, - 'nb_projects' => $nb_projects, - 'title' => t('Projects').' ('.$nb_projects.')' - ))); - } - - /** - * Show the project information page - * - * @access public - */ - public function show() - { - $project = $this->getProject(); - - $this->response->html($this->helper->layout->project('project/show', array( - 'project' => $project, - 'stats' => $this->project->getTaskStats($project['id']), - 'title' => $project['name'], - ))); - } - - /** - * Public access management - * - * @access public - */ - public function share() - { - $project = $this->getProject(); - $switch = $this->request->getStringParam('switch'); - - if ($switch === 'enable' || $switch === 'disable') { - $this->checkCSRFParam(); - - if ($this->project->{$switch.'PublicAccess'}($project['id'])) { - $this->flash->success(t('Project updated successfully.')); - } else { - $this->flash->failure(t('Unable to update this project.')); - } - - $this->response->redirect($this->helper->url->to('project', 'share', array('project_id' => $project['id']))); - } - - $this->response->html($this->helper->layout->project('project/share', array( - 'project' => $project, - 'title' => t('Public access'), - ))); - } - - /** - * Integrations page - * - * @access public - */ - public function integrations() - { - $project = $this->getProject(); - - if ($this->request->isPost()) { - $this->projectMetadata->save($project['id'], $this->request->getValues()); - $this->flash->success(t('Project updated successfully.')); - $this->response->redirect($this->helper->url->to('project', 'integrations', array('project_id' => $project['id']))); - } - - $this->response->html($this->helper->layout->project('project/integrations', array( - 'project' => $project, - 'title' => t('Integrations'), - 'webhook_token' => $this->config->get('webhook_token'), - 'values' => $this->projectMetadata->getAll($project['id']), - 'errors' => array(), - ))); - } - - /** - * Display project notifications - * - * @access public - */ - public function notifications() - { - $project = $this->getProject(); - - if ($this->request->isPost()) { - $values = $this->request->getValues(); - $this->projectNotification->saveSettings($project['id'], $values); - $this->flash->success(t('Project updated successfully.')); - $this->response->redirect($this->helper->url->to('project', 'notifications', array('project_id' => $project['id']))); - } - - $this->response->html($this->helper->layout->project('project/notifications', array( - 'notifications' => $this->projectNotification->readSettings($project['id']), - 'types' => $this->projectNotificationType->getTypes(), - 'project' => $project, - 'title' => t('Notifications'), - ))); - } - - /** - * Remove a project - * - * @access public - */ - public function remove() - { - $project = $this->getProject(); - - if ($this->request->getStringParam('remove') === 'yes') { - $this->checkCSRFParam(); - - if ($this->project->remove($project['id'])) { - $this->flash->success(t('Project removed successfully.')); - } else { - $this->flash->failure(t('Unable to remove this project.')); - } - - $this->response->redirect($this->helper->url->to('project', 'index')); - } - - $this->response->html($this->helper->layout->project('project/remove', array( - 'project' => $project, - 'title' => t('Remove project') - ))); - } - - /** - * Duplicate a project - * - * @author Antonio Rabelo - * @author Michael Lüpkes - * @access public - */ - public function duplicate() - { - $project = $this->getProject(); - - if ($this->request->getStringParam('duplicate') === 'yes') { - $project_id = $this->projectDuplication->duplicate($project['id'], array_keys($this->request->getValues()), $this->userSession->getId()); - - if ($project_id !== false) { - $this->flash->success(t('Project cloned successfully.')); - } else { - $this->flash->failure(t('Unable to clone this project.')); - } - - $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project_id))); - } - - $this->response->html($this->helper->layout->project('project/duplicate', array( - 'project' => $project, - 'title' => t('Clone this project') - ))); - } - - /** - * Disable a project - * - * @access public - */ - public function disable() - { - $project = $this->getProject(); - - if ($this->request->getStringParam('disable') === 'yes') { - $this->checkCSRFParam(); - - if ($this->project->disable($project['id'])) { - $this->flash->success(t('Project disabled successfully.')); - } else { - $this->flash->failure(t('Unable to disable this project.')); - } - - $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project['id']))); - } - - $this->response->html($this->helper->layout->project('project/disable', array( - 'project' => $project, - 'title' => t('Project activation') - ))); - } - - /** - * Enable a project - * - * @access public - */ - public function enable() - { - $project = $this->getProject(); - - if ($this->request->getStringParam('enable') === 'yes') { - $this->checkCSRFParam(); - - if ($this->project->enable($project['id'])) { - $this->flash->success(t('Project activated successfully.')); - } else { - $this->flash->failure(t('Unable to activate this project.')); - } - - $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project['id']))); - } - - $this->response->html($this->helper->layout->project('project/enable', array( - 'project' => $project, - 'title' => t('Project activation') - ))); - } -} diff --git a/sources/app/Controller/ActionProject.php b/sources/app/Controller/ProjectActionDuplicationController.php similarity index 55% rename from sources/app/Controller/ActionProject.php rename to sources/app/Controller/ProjectActionDuplicationController.php index e5063f7..a4993cc 100644 --- a/sources/app/Controller/ActionProject.php +++ b/sources/app/Controller/ProjectActionDuplicationController.php @@ -5,18 +5,18 @@ namespace Kanboard\Controller; /** * Duplicate automatic action from another project * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class ActionProject extends Base +class ProjectActionDuplicationController extends BaseController { - public function project() + public function show() { $project = $this->getProject(); - $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId()); + $projects = $this->projectUserRoleModel->getProjectsByUser($this->userSession->getId()); unset($projects[$project['id']]); - $this->response->html($this->template->render('action_project/project', array( + $this->response->html($this->template->render('project_action_duplication/show', array( 'project' => $project, 'projects_list' => $projects, ))); @@ -27,12 +27,12 @@ class ActionProject extends Base $project = $this->getProject(); $src_project_id = $this->request->getValue('src_project_id'); - if ($this->action->duplicate($src_project_id, $project['id'])) { + if ($this->actionModel->duplicate($src_project_id, $project['id'])) { $this->flash->success(t('Actions duplicated successfully.')); } else { $this->flash->failure(t('Unable to duplicate actions.')); } - $this->response->redirect($this->helper->url->to('action', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ActionController', 'index', array('project_id' => $project['id']))); } } diff --git a/sources/app/Controller/ProjectCreation.php b/sources/app/Controller/ProjectCreationController.php similarity index 81% rename from sources/app/Controller/ProjectCreation.php rename to sources/app/Controller/ProjectCreationController.php index 88f41fc..c471cfd 100644 --- a/sources/app/Controller/ProjectCreation.php +++ b/sources/app/Controller/ProjectCreationController.php @@ -5,20 +5,22 @@ namespace Kanboard\Controller; /** * Project Creation Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class ProjectCreation extends Base +class ProjectCreationController extends BaseController { /** * Display a form to create a new project * * @access public + * @param array $values + * @param array $errors */ public function create(array $values = array(), array $errors = array()) { $is_private = isset($values['is_private']) && $values['is_private'] == 1; - $projects_list = array(0 => t('Do not duplicate anything')) + $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); + $projects_list = array(0 => t('Do not duplicate anything')) + $this->projectUserRoleModel->getActiveProjectsByUser($this->userSession->getId()); $this->response->html($this->helper->layout->app('project_creation/create', array( 'values' => $values, @@ -33,6 +35,8 @@ class ProjectCreation extends Base * Display a form to create a private project * * @access public + * @param array $values + * @param array $errors */ public function createPrivate(array $values = array(), array $errors = array()) { @@ -55,13 +59,13 @@ class ProjectCreation extends Base if ($project_id > 0) { $this->flash->success(t('Your project have been created successfully.')); - return $this->response->redirect($this->helper->url->to('project', 'show', array('project_id' => $project_id))); + return $this->response->redirect($this->helper->url->to('ProjectViewController', 'show', array('project_id' => $project_id))); } $this->flash->failure(t('Unable to create your project.')); } - $this->create($values, $errors); + return $this->create($values, $errors); } /** @@ -94,7 +98,7 @@ class ProjectCreation extends Base 'is_private' => $values['is_private'], ); - return $this->project->create($project, $this->userSession->getId(), true); + return $this->projectModel->create($project, $this->userSession->getId(), true); } /** @@ -108,13 +112,13 @@ class ProjectCreation extends Base { $selection = array(); - foreach ($this->projectDuplication->getOptionalSelection() as $item) { + foreach ($this->projectDuplicationModel->getOptionalSelection() as $item) { if (isset($values[$item]) && $values[$item] == 1) { $selection[] = $item; } } - return $this->projectDuplication->duplicate( + return $this->projectDuplicationModel->duplicate( $values['src_project_id'], $selection, $this->userSession->getId(), diff --git a/sources/app/Controller/ProjectEdit.php b/sources/app/Controller/ProjectEditController.php similarity index 78% rename from sources/app/Controller/ProjectEdit.php rename to sources/app/Controller/ProjectEditController.php index 94be020..228d681 100644 --- a/sources/app/Controller/ProjectEdit.php +++ b/sources/app/Controller/ProjectEditController.php @@ -5,15 +5,17 @@ namespace Kanboard\Controller; /** * Project Edit Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class ProjectEdit extends Base +class ProjectEditController extends BaseController { /** * General edition (most common operations) * * @access public + * @param array $values + * @param array $errors */ public function edit(array $values = array(), array $errors = array()) { @@ -24,6 +26,8 @@ class ProjectEdit extends Base * Change start and end dates * * @access public + * @param array $values + * @param array $errors */ public function dates(array $values = array(), array $errors = array()) { @@ -34,6 +38,8 @@ class ProjectEdit extends Base * Change project description * * @access public + * @param array $values + * @param array $errors */ public function description(array $values = array(), array $errors = array()) { @@ -44,6 +50,8 @@ class ProjectEdit extends Base * Change task priority * * @access public + * @param array $values + * @param array $errors */ public function priority(array $values = array(), array $errors = array()) { @@ -65,15 +73,15 @@ class ProjectEdit extends Base list($valid, $errors) = $this->projectValidator->validateModification($values); if ($valid) { - if ($this->project->update($values)) { + if ($this->projectModel->update($values)) { $this->flash->success(t('Project updated successfully.')); - $this->response->redirect($this->helper->url->to('ProjectEdit', $redirect, array('project_id' => $project['id'])), true); + return $this->response->redirect($this->helper->url->to('ProjectEditController', $redirect, array('project_id' => $project['id'])), true); } else { $this->flash->failure(t('Unable to update this project.')); } } - $this->$redirect($values, $errors); + return $this->$redirect($values, $errors); } /** @@ -89,11 +97,11 @@ class ProjectEdit extends Base { if ($redirect === 'edit') { if (isset($values['is_private'])) { - if (! $this->helper->user->hasProjectAccess('ProjectCreation', 'create', $project['id'])) { + if (! $this->helper->user->hasProjectAccess('ProjectCreationController', 'create', $project['id'])) { unset($values['is_private']); } } elseif ($project['is_private'] == 1 && ! isset($values['is_private'])) { - if ($this->helper->user->hasProjectAccess('ProjectCreation', 'create', $project['id'])) { + if ($this->helper->user->hasProjectAccess('ProjectCreationController', 'create', $project['id'])) { $values += array('is_private' => 0); } } @@ -103,7 +111,7 @@ class ProjectEdit extends Base } /** - * Common metthod to render different views + * Common method to render different views * * @access private * @param string $template @@ -115,7 +123,7 @@ class ProjectEdit extends Base $project = $this->getProject(); $this->response->html($this->helper->layout->project($template, array( - 'owners' => $this->projectUserRole->getAssignableUsersList($project['id'], true), + 'owners' => $this->projectUserRoleModel->getAssignableUsersList($project['id'], true), 'values' => empty($values) ? $project : $values, 'errors' => $errors, 'project' => $project, diff --git a/sources/app/Controller/ProjectFile.php b/sources/app/Controller/ProjectFileController.php similarity index 72% rename from sources/app/Controller/ProjectFile.php rename to sources/app/Controller/ProjectFileController.php index 96764a9..cbe4867 100644 --- a/sources/app/Controller/ProjectFile.php +++ b/sources/app/Controller/ProjectFileController.php @@ -5,10 +5,10 @@ namespace Kanboard\Controller; /** * Project File Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class ProjectFile extends Base +class ProjectFileController extends BaseController { /** * File upload form @@ -34,11 +34,11 @@ class ProjectFile extends Base { $project = $this->getProject(); - if (! $this->projectFile->uploadFiles($project['id'], $this->request->getFileInfo('files'))) { + if (! $this->projectFileModel->uploadFiles($project['id'], $this->request->getFileInfo('files'))) { $this->flash->failure(t('Unable to upload the file.')); } - $this->response->redirect($this->helper->url->to('ProjectOverview', 'show', array('project_id' => $project['id'])), true); + $this->response->redirect($this->helper->url->to('ProjectOverviewController', 'show', array('project_id' => $project['id'])), true); } /** @@ -50,15 +50,15 @@ class ProjectFile extends Base { $this->checkCSRFParam(); $project = $this->getProject(); - $file = $this->projectFile->getById($this->request->getIntegerParam('file_id')); + $file = $this->projectFileModel->getById($this->request->getIntegerParam('file_id')); - if ($this->projectFile->remove($file['id'])) { + if ($this->projectFileModel->remove($file['id'])) { $this->flash->success(t('File removed successfully.')); } else { $this->flash->failure(t('Unable to remove this file.')); } - $this->response->redirect($this->helper->url->to('ProjectOverview', 'show', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ProjectOverviewController', 'show', array('project_id' => $project['id']))); } /** @@ -69,7 +69,7 @@ class ProjectFile extends Base public function confirm() { $project = $this->getProject(); - $file = $this->projectFile->getById($this->request->getIntegerParam('file_id')); + $file = $this->projectFileModel->getById($this->request->getIntegerParam('file_id')); $this->response->html($this->template->render('project_file/remove', array( 'project' => $project, diff --git a/sources/app/Controller/ProjectGanttController.php b/sources/app/Controller/ProjectGanttController.php new file mode 100644 index 0000000..a70d9ee --- /dev/null +++ b/sources/app/Controller/ProjectGanttController.php @@ -0,0 +1,57 @@ +projectPermissionModel->getActiveProjectIds($this->userSession->getId()); + $filter = $this->projectQuery + ->withFilter(new ProjectTypeFilter(ProjectModel::TYPE_TEAM)) + ->withFilter(new ProjectStatusFilter(ProjectModel::ACTIVE)) + ->withFilter(new ProjectIdsFilter($project_ids)); + + $filter->getQuery()->asc(ProjectModel::TABLE.'.start_date'); + + $this->response->html($this->helper->layout->app('project_gantt/show', array( + 'projects' => $filter->format(new ProjectGanttFormatter($this->container)), + 'title' => t('Gantt chart for all projects'), + ))); + } + + /** + * Save new project start date and end date + */ + public function save() + { + $values = $this->request->getJson(); + + $result = $this->projectModel->update(array( + 'id' => $values['id'], + 'start_date' => $this->dateParser->getIsoDate(strtotime($values['start'])), + 'end_date' => $this->dateParser->getIsoDate(strtotime($values['end'])), + )); + + if (! $result) { + $this->response->json(array('message' => 'Unable to save project'), 400); + } else { + $this->response->json(array('message' => 'OK'), 201); + } + } +} diff --git a/sources/app/Controller/ProjectListController.php b/sources/app/Controller/ProjectListController.php new file mode 100644 index 0000000..e117240 --- /dev/null +++ b/sources/app/Controller/ProjectListController.php @@ -0,0 +1,41 @@ +userSession->isAdmin()) { + $project_ids = $this->projectModel->getAllIds(); + } else { + $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); + } + + $nb_projects = count($project_ids); + + $paginator = $this->paginator + ->setUrl('ProjectListController', 'show') + ->setMax(20) + ->setOrder('name') + ->setQuery($this->projectModel->getQueryColumnStats($project_ids)) + ->calculate(); + + $this->response->html($this->helper->layout->app('project_list/show', array( + 'paginator' => $paginator, + 'nb_projects' => $nb_projects, + 'title' => t('Projects').' ('.$nb_projects.')' + ))); + } +} diff --git a/sources/app/Controller/ProjectOverview.php b/sources/app/Controller/ProjectOverviewController.php similarity index 53% rename from sources/app/Controller/ProjectOverview.php rename to sources/app/Controller/ProjectOverviewController.php index 0464580..abdff65 100644 --- a/sources/app/Controller/ProjectOverview.php +++ b/sources/app/Controller/ProjectOverviewController.php @@ -5,10 +5,10 @@ namespace Kanboard\Controller; /** * Project Overview Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class ProjectOverview extends Base +class ProjectOverviewController extends BaseController { /** * Show project overview @@ -16,17 +16,17 @@ class ProjectOverview extends Base public function show() { $project = $this->getProject(); - $this->project->getColumnStats($project); + $this->projectModel->getColumnStats($project); $this->response->html($this->helper->layout->app('project_overview/show', array( 'project' => $project, 'title' => $project['name'], 'description' => $this->helper->projectHeader->getDescription($project), - 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']), + 'users' => $this->projectUserRoleModel->getAllUsersGroupedByRole($project['id']), 'roles' => $this->role->getProjectRoles(), - 'events' => $this->projectActivity->getProject($project['id'], 10), - 'images' => $this->projectFile->getAllImages($project['id']), - 'files' => $this->projectFile->getAllDocuments($project['id']), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id'], 10), + 'images' => $this->projectFileModel->getAllImages($project['id']), + 'files' => $this->projectFileModel->getAllDocuments($project['id']), ))); } } diff --git a/sources/app/Controller/ProjectPermission.php b/sources/app/Controller/ProjectPermissionController.php similarity index 71% rename from sources/app/Controller/ProjectPermission.php rename to sources/app/Controller/ProjectPermissionController.php index 800da02..f3ca6ed 100644 --- a/sources/app/Controller/ProjectPermission.php +++ b/sources/app/Controller/ProjectPermissionController.php @@ -2,15 +2,16 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\AccessForbiddenException; use Kanboard\Core\Security\Role; /** - * Project Permission + * Project Permission Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class ProjectPermission extends Base +class ProjectPermissionController extends BaseController { /** * Permissions are only available for team projects @@ -18,13 +19,14 @@ class ProjectPermission extends Base * @access protected * @param integer $project_id Default project id * @return array + * @throws AccessForbiddenException */ protected function getProject($project_id = 0) { $project = parent::getProject($project_id); if ($project['is_private'] == 1) { - $this->forbidden(); + throw new AccessForbiddenException(); } return $project; @@ -34,6 +36,9 @@ class ProjectPermission extends Base * Show all permissions * * @access public + * @param array $values + * @param array $errors + * @throws AccessForbiddenException */ public function index(array $values = array(), array $errors = array()) { @@ -45,8 +50,8 @@ class ProjectPermission extends Base $this->response->html($this->helper->layout->project('project_permission/index', array( 'project' => $project, - 'users' => $this->projectUserRole->getUsers($project['id']), - 'groups' => $this->projectGroupRole->getGroups($project['id']), + 'users' => $this->projectUserRoleModel->getUsers($project['id']), + 'groups' => $this->projectGroupRoleModel->getGroups($project['id']), 'roles' => $this->role->getProjectRoles(), 'values' => $values, 'errors' => $errors, @@ -64,13 +69,13 @@ class ProjectPermission extends Base $project = $this->getProject(); $values = $this->request->getValues() + array('is_everybody_allowed' => 0); - if ($this->project->update($values)) { + if ($this->projectModel->update($values)) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ProjectPermissionController', 'index', array('project_id' => $project['id']))); } /** @@ -83,13 +88,15 @@ class ProjectPermission extends Base $project = $this->getProject(); $values = $this->request->getValues(); - if ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) { + if (empty($values['user_id'])) { + $this->flash->failure(t('User not found.')); + } elseif ($this->projectUserRoleModel->addUser($values['project_id'], $values['user_id'], $values['role'])) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ProjectPermissionController', 'index', array('project_id' => $project['id']))); } /** @@ -103,13 +110,13 @@ class ProjectPermission extends Base $project = $this->getProject(); $user_id = $this->request->getIntegerParam('user_id'); - if ($this->projectUserRole->removeUser($project['id'], $user_id)) { + if ($this->projectUserRoleModel->removeUser($project['id'], $user_id)) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ProjectPermissionController', 'index', array('project_id' => $project['id']))); } /** @@ -122,7 +129,7 @@ class ProjectPermission extends Base $project = $this->getProject(); $values = $this->request->getJson(); - if (! empty($project) && ! empty($values) && $this->projectUserRole->changeUserRole($project['id'], $values['id'], $values['role'])) { + if (! empty($project) && ! empty($values) && $this->projectUserRoleModel->changeUserRole($project['id'], $values['id'], $values['role'])) { $this->response->json(array('status' => 'ok')); } else { $this->response->json(array('status' => 'error')); @@ -140,16 +147,16 @@ class ProjectPermission extends Base $values = $this->request->getValues(); if (empty($values['group_id']) && ! empty($values['external_id'])) { - $values['group_id'] = $this->group->create($values['name'], $values['external_id']); + $values['group_id'] = $this->groupModel->create($values['name'], $values['external_id']); } - if ($this->projectGroupRole->addGroup($project['id'], $values['group_id'], $values['role'])) { + if ($this->projectGroupRoleModel->addGroup($project['id'], $values['group_id'], $values['role'])) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ProjectPermissionController', 'index', array('project_id' => $project['id']))); } /** @@ -163,13 +170,13 @@ class ProjectPermission extends Base $project = $this->getProject(); $group_id = $this->request->getIntegerParam('group_id'); - if ($this->projectGroupRole->removeGroup($project['id'], $group_id)) { + if ($this->projectGroupRoleModel->removeGroup($project['id'], $group_id)) { $this->flash->success(t('Project updated successfully.')); } else { $this->flash->failure(t('Unable to update this project.')); } - $this->response->redirect($this->helper->url->to('ProjectPermission', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('ProjectPermissionController', 'index', array('project_id' => $project['id']))); } /** @@ -182,7 +189,7 @@ class ProjectPermission extends Base $project = $this->getProject(); $values = $this->request->getJson(); - if (! empty($project) && ! empty($values) && $this->projectGroupRole->changeGroupRole($project['id'], $values['id'], $values['role'])) { + if (! empty($project) && ! empty($values) && $this->projectGroupRoleModel->changeGroupRole($project['id'], $values['id'], $values['role'])) { $this->response->json(array('status' => 'ok')); } else { $this->response->json(array('status' => 'error')); diff --git a/sources/app/Controller/ProjectStatusController.php b/sources/app/Controller/ProjectStatusController.php new file mode 100644 index 0000000..78e7787 --- /dev/null +++ b/sources/app/Controller/ProjectStatusController.php @@ -0,0 +1,102 @@ +getProject(); + + $this->response->html($this->template->render('project_status/enable', array( + 'project' => $project, + 'title' => t('Project activation') + ))); + } + + /** + * Enable the project + */ + public function enable() + { + $project = $this->getProject(); + $this->checkCSRFParam(); + + if ($this->projectModel->enable($project['id'])) { + $this->flash->success(t('Project activated successfully.')); + } else { + $this->flash->failure(t('Unable to activate this project.')); + } + + $this->response->redirect($this->helper->url->to('ProjectViewController', 'show', array('project_id' => $project['id'])), true); + } + + /** + * Disable a project (confirmation dialog box) + */ + public function confirmDisable() + { + $project = $this->getProject(); + + $this->response->html($this->template->render('project_status/disable', array( + 'project' => $project, + 'title' => t('Project activation') + ))); + } + + /** + * Disable a project + */ + public function disable() + { + $project = $this->getProject(); + $this->checkCSRFParam(); + + if ($this->projectModel->disable($project['id'])) { + $this->flash->success(t('Project disabled successfully.')); + } else { + $this->flash->failure(t('Unable to disable this project.')); + } + + $this->response->redirect($this->helper->url->to('ProjectViewController', 'show', array('project_id' => $project['id'])), true); + } + + /** + * Remove a project (confirmation dialog box) + */ + public function confirmRemove() + { + $project = $this->getProject(); + + $this->response->html($this->template->render('project_status/remove', array( + 'project' => $project, + 'title' => t('Remove project') + ))); + } + + /** + * Remove a project + */ + public function remove() + { + $project = $this->getProject(); + $this->checkCSRFParam(); + + if ($this->projectModel->remove($project['id'])) { + $this->flash->success(t('Project removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this project.')); + } + + $this->response->redirect($this->helper->url->to('ProjectListController', 'show'), true); + } +} diff --git a/sources/app/Controller/ProjectTagController.php b/sources/app/Controller/ProjectTagController.php new file mode 100644 index 0000000..acf514d --- /dev/null +++ b/sources/app/Controller/ProjectTagController.php @@ -0,0 +1,134 @@ +getProject(); + + $this->response->html($this->helper->layout->project('project_tag/index', array( + 'project' => $project, + 'tags' => $this->tagModel->getAllByProject($project['id']), + 'title' => t('Project tags management'), + ))); + } + + public function create(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + if (empty($values)) { + $values['project_id'] = $project['id']; + } + + $this->response->html($this->template->render('project_tag/create', array( + 'project' => $project, + 'values' => $values, + 'errors' => $errors, + ))); + } + + public function save() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + list($valid, $errors) = $this->tagValidator->validateCreation($values); + + if ($valid) { + if ($this->tagModel->create($project['id'], $values['name']) > 0) { + $this->flash->success(t('Tag created successfully.')); + } else { + $this->flash->failure(t('Unable to create this tag.')); + } + + $this->response->redirect($this->helper->url->to('ProjectTagController', 'index', array('project_id' => $project['id']))); + } else { + $this->create($values, $errors); + } + } + + public function edit(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $tag_id = $this->request->getIntegerParam('tag_id'); + $tag = $this->tagModel->getById($tag_id); + + if (empty($values)) { + $values = $tag; + } + + $this->response->html($this->template->render('project_tag/edit', array( + 'project' => $project, + 'tag' => $tag, + 'values' => $values, + 'errors' => $errors, + ))); + } + + public function update() + { + $project = $this->getProject(); + $tag_id = $this->request->getIntegerParam('tag_id'); + $tag = $this->tagModel->getById($tag_id); + $values = $this->request->getValues(); + list($valid, $errors) = $this->tagValidator->validateModification($values); + + if ($tag['project_id'] != $project['id']) { + throw new AccessForbiddenException(); + } + + if ($valid) { + if ($this->tagModel->update($values['id'], $values['name'])) { + $this->flash->success(t('Tag updated successfully.')); + } else { + $this->flash->failure(t('Unable to update this tag.')); + } + + $this->response->redirect($this->helper->url->to('ProjectTagController', 'index', array('project_id' => $project['id']))); + } else { + $this->edit($values, $errors); + } + } + + public function confirm() + { + $project = $this->getProject(); + $tag_id = $this->request->getIntegerParam('tag_id'); + $tag = $this->tagModel->getById($tag_id); + + $this->response->html($this->template->render('project_tag/remove', array( + 'tag' => $tag, + 'project' => $project, + ))); + } + + public function remove() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $tag_id = $this->request->getIntegerParam('tag_id'); + $tag = $this->tagModel->getById($tag_id); + + if ($tag['project_id'] != $project['id']) { + throw new AccessForbiddenException(); + } + + if ($this->tagModel->remove($tag_id)) { + $this->flash->success(t('Tag removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this tag.')); + } + + $this->response->redirect($this->helper->url->to('ProjectTagController', 'index', array('project_id' => $project['id']))); + } +} diff --git a/sources/app/Controller/Projectuser.php b/sources/app/Controller/ProjectUserOverviewController.php similarity index 72% rename from sources/app/Controller/Projectuser.php rename to sources/app/Controller/ProjectUserOverviewController.php index a6d4fe4..686de83 100644 --- a/sources/app/Controller/Projectuser.php +++ b/sources/app/Controller/ProjectUserOverviewController.php @@ -2,36 +2,36 @@ namespace Kanboard\Controller; -use Kanboard\Model\User as UserModel; -use Kanboard\Model\Task as TaskModel; +use Kanboard\Model\UserModel; +use Kanboard\Model\TaskModel; use Kanboard\Core\Security\Role; /** * Project User overview * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Projectuser extends Base +class ProjectUserOverviewController extends BaseController { private function common() { $user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID); if ($this->userSession->isAdmin()) { - $project_ids = $this->project->getAllIds(); + $project_ids = $this->projectModel->getAllIds(); } else { - $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); + $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); } - return array($user_id, $project_ids, $this->user->getActiveUsersList(true)); + return array($user_id, $project_ids, $this->userModel->getActiveUsersList(true)); } private function role($role, $action, $title, $title_user) { list($user_id, $project_ids, $users) = $this->common(); - $query = $this->projectPermission->getQueryByRole($project_ids, $role)->callback(array($this->project, 'applyColumnStats')); + $query = $this->projectPermissionModel->getQueryByRole($project_ids, $role)->callback(array($this->projectModel, 'applyColumnStats')); if ($user_id !== UserModel::EVERYBODY_ID && isset($users[$user_id])) { $query->eq(UserModel::TABLE.'.id', $user_id); @@ -39,13 +39,13 @@ class Projectuser extends Base } $paginator = $this->paginator - ->setUrl('projectuser', $action, array('user_id' => $user_id)) + ->setUrl('ProjectUserOverviewController', $action, array('user_id' => $user_id)) ->setMax(30) ->setOrder('projects.name') ->setQuery($query) ->calculate(); - $this->response->html($this->helper->layout->projectUser('project_user/roles', array( + $this->response->html($this->helper->layout->projectUser('project_user_overview/roles', array( 'paginator' => $paginator, 'title' => $title, 'user_id' => $user_id, @@ -57,7 +57,7 @@ class Projectuser extends Base { list($user_id, $project_ids, $users) = $this->common(); - $query = $this->taskFinder->getProjectUserOverviewQuery($project_ids, $is_active); + $query = $this->taskFinderModel->getProjectUserOverviewQuery($project_ids, $is_active); if ($user_id !== UserModel::EVERYBODY_ID && isset($users[$user_id])) { $query->eq(TaskModel::TABLE.'.owner_id', $user_id); @@ -65,13 +65,13 @@ class Projectuser extends Base } $paginator = $this->paginator - ->setUrl('projectuser', $action, array('user_id' => $user_id)) + ->setUrl('ProjectUserOverviewController', $action, array('user_id' => $user_id)) ->setMax(50) ->setOrder(TaskModel::TABLE.'.id') ->setQuery($query) ->calculate(); - $this->response->html($this->helper->layout->projectUser('project_user/tasks', array( + $this->response->html($this->helper->layout->projectUser('project_user_overview/tasks', array( 'paginator' => $paginator, 'title' => $title, 'user_id' => $user_id, @@ -94,7 +94,7 @@ class Projectuser extends Base */ public function members() { - $this->role(ROLE::PROJECT_MEMBER, 'members', t('People who are project members'), 'Projects where "%s" is member'); + $this->role(Role::PROJECT_MEMBER, 'members', t('People who are project members'), 'Projects where "%s" is member'); } /** @@ -122,8 +122,8 @@ class Projectuser extends Base { $project = $this->getProject(); - return $this->response->html($this->template->render('project_user/tooltip_users', array( - 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']), + return $this->response->html($this->template->render('project_user_overview/tooltip_users', array( + 'users' => $this->projectUserRoleModel->getAllUsersGroupedByRole($project['id']), 'roles' => $this->role->getProjectRoles(), ))); } diff --git a/sources/app/Controller/ProjectViewController.php b/sources/app/Controller/ProjectViewController.php new file mode 100644 index 0000000..92b9380 --- /dev/null +++ b/sources/app/Controller/ProjectViewController.php @@ -0,0 +1,162 @@ +getProject(); + + $this->response->html($this->helper->layout->project('project_view/show', array( + 'project' => $project, + 'stats' => $this->projectModel->getTaskStats($project['id']), + 'title' => $project['name'], + ))); + } + + /** + * Public access management + * + * @access public + */ + public function share() + { + $project = $this->getProject(); + + $this->response->html($this->helper->layout->project('project_view/share', array( + 'project' => $project, + 'title' => t('Public access'), + ))); + } + + /** + * Change project sharing + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + * @throws \Kanboard\Core\Controller\PageNotFoundException + */ + public function updateSharing() + { + $project = $this->getProject(); + $this->checkCSRFParam(); + $switch = $this->request->getStringParam('switch'); + + if ($this->projectModel->{$switch.'PublicAccess'}($project['id'])) { + $this->flash->success(t('Project updated successfully.')); + } else { + $this->flash->failure(t('Unable to update this project.')); + } + + $this->response->redirect($this->helper->url->to('ProjectViewController', 'share', array('project_id' => $project['id']))); + } + + /** + * Integrations page + * + * @access public + */ + public function integrations() + { + $project = $this->getProject(); + + $this->response->html($this->helper->layout->project('project_view/integrations', array( + 'project' => $project, + 'title' => t('Integrations'), + 'webhook_token' => $this->configModel->get('webhook_token'), + 'values' => $this->projectMetadataModel->getAll($project['id']), + 'errors' => array(), + ))); + } + + /** + * Update integrations + * + * @throws \Kanboard\Core\Controller\PageNotFoundException + */ + public function updateIntegrations() + { + $project = $this->getProject(); + + $this->projectMetadataModel->save($project['id'], $this->request->getValues()); + $this->flash->success(t('Project updated successfully.')); + $this->response->redirect($this->helper->url->to('ProjectViewController', 'integrations', array('project_id' => $project['id']))); + } + + /** + * Display project notifications + * + * @access public + */ + public function notifications() + { + $project = $this->getProject(); + + $this->response->html($this->helper->layout->project('project_view/notifications', array( + 'notifications' => $this->projectNotificationModel->readSettings($project['id']), + 'types' => $this->projectNotificationTypeModel->getTypes(), + 'project' => $project, + 'title' => t('Notifications'), + ))); + } + + /** + * Update notifications + * + * @throws \Kanboard\Core\Controller\PageNotFoundException + */ + public function updateNotifications() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $this->projectNotificationModel->saveSettings($project['id'], $values); + $this->flash->success(t('Project updated successfully.')); + $this->response->redirect($this->helper->url->to('ProjectViewController', 'notifications', array('project_id' => $project['id']))); + } + + /** + * Duplicate a project + * + * @author Antonio Rabelo + * @author Michael Lüpkes + * @access public + */ + public function duplicate() + { + $project = $this->getProject(); + + $this->response->html($this->helper->layout->project('project_view/duplicate', array( + 'project' => $project, + 'title' => t('Clone this project') + ))); + } + + /** + * Do project duplication + */ + public function doDuplication() + { + $project = $this->getProject(); + $project_id = $this->projectDuplicationModel->duplicate($project['id'], array_keys($this->request->getValues()), $this->userSession->getId()); + + if ($project_id !== false) { + $this->flash->success(t('Project cloned successfully.')); + } else { + $this->flash->failure(t('Unable to clone this project.')); + } + + $this->response->redirect($this->helper->url->to('ProjectViewController', 'show', array('project_id' => $project_id))); + } +} diff --git a/sources/app/Controller/Search.php b/sources/app/Controller/Search.php deleted file mode 100644 index 9b9b9e6..0000000 --- a/sources/app/Controller/Search.php +++ /dev/null @@ -1,49 +0,0 @@ -projectUserRole->getProjectsByUser($this->userSession->getId()); - $search = urldecode($this->request->getStringParam('search')); - $nb_tasks = 0; - - $paginator = $this->paginator - ->setUrl('search', 'index', array('search' => $search)) - ->setMax(30) - ->setOrder('tasks.id') - ->setDirection('DESC'); - - if ($search !== '' && ! empty($projects)) { - $query = $this - ->taskFilter - ->search($search) - ->filterByProjects(array_keys($projects)) - ->getQuery(); - - $paginator - ->setQuery($query) - ->calculate(); - - $nb_tasks = $paginator->getTotal(); - } - - $this->response->html($this->helper->layout->app('search/index', array( - 'values' => array( - 'search' => $search, - 'controller' => 'search', - 'action' => 'index', - ), - 'paginator' => $paginator, - 'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') - ))); - } -} diff --git a/sources/app/Controller/SearchController.php b/sources/app/Controller/SearchController.php new file mode 100644 index 0000000..8557b18 --- /dev/null +++ b/sources/app/Controller/SearchController.php @@ -0,0 +1,67 @@ +projectUserRoleModel->getProjectsByUser($this->userSession->getId()); + $search = urldecode($this->request->getStringParam('search')); + $nb_tasks = 0; + + $paginator = $this->paginator + ->setUrl('SearchController', 'index', array('search' => $search)) + ->setMax(30) + ->setOrder('tasks.id') + ->setDirection('DESC'); + + if ($search !== '' && ! empty($projects)) { + $paginator + ->setQuery($this->taskLexer + ->build($search) + ->withFilter(new TaskProjectsFilter(array_keys($projects))) + ->getQuery() + ) + ->calculate(); + + $nb_tasks = $paginator->getTotal(); + } + + $this->response->html($this->helper->layout->app('search/index', array( + 'values' => array( + 'search' => $search, + 'controller' => 'SearchController', + 'action' => 'index', + ), + 'paginator' => $paginator, + 'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') + ))); + } + + public function activity() + { + $search = urldecode($this->request->getStringParam('search')); + $events = $this->helper->projectActivity->searchEvents($search); + $nb_events = count($events); + + $this->response->html($this->helper->layout->app('search/activity', array( + 'values' => array( + 'search' => $search, + 'controller' => 'SearchController', + 'action' => 'activity', + ), + 'title' => t('Search in activity stream').($nb_events > 0 ? ' ('.$nb_events.')' : ''), + 'nb_events' => $nb_events, + 'events' => $events, + ))); + } +} diff --git a/sources/app/Controller/Subtask.php b/sources/app/Controller/SubtaskController.php similarity index 67% rename from sources/app/Controller/Subtask.php rename to sources/app/Controller/SubtaskController.php index dea2b08..93dab5c 100644 --- a/sources/app/Controller/Subtask.php +++ b/sources/app/Controller/SubtaskController.php @@ -2,18 +2,25 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\AccessForbiddenException; +use Kanboard\Core\Controller\PageNotFoundException; + /** * Subtask controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Subtask extends Base +class SubtaskController extends BaseController { /** * Creation form * * @access public + * @param array $values + * @param array $errors + * @throws AccessForbiddenException + * @throws PageNotFoundException */ public function create(array $values = array(), array $errors = array()) { @@ -29,7 +36,7 @@ class Subtask extends Base $this->response->html($this->template->render('subtask/create', array( 'values' => $values, 'errors' => $errors, - 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']), + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($task['project_id']), 'task' => $task, ))); } @@ -47,7 +54,7 @@ class Subtask extends Base list($valid, $errors) = $this->subtaskValidator->validateCreation($values); if ($valid) { - if ($this->subtask->create($values)) { + if ($this->subtaskModel->create($values) !== false) { $this->flash->success(t('Sub-task added successfully.')); } else { $this->flash->failure(t('Unable to create your sub-task.')); @@ -57,27 +64,31 @@ class Subtask extends Base return $this->create(array('project_id' => $task['project_id'], 'task_id' => $task['id'], 'another_subtask' => 1)); } - return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks'), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']), 'subtasks'), true); } - $this->create($values, $errors); + return $this->create($values, $errors); } /** * Edit form * * @access public + * @param array $values + * @param array $errors + * @throws AccessForbiddenException + * @throws PageNotFoundException */ public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); - $subtask = $this->getSubTask(); + $subtask = $this->getSubtask(); $this->response->html($this->template->render('subtask/edit', array( 'values' => empty($values) ? $subtask : $values, 'errors' => $errors, - 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']), - 'status_list' => $this->subtask->getStatusList(), + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($task['project_id']), + 'status_list' => $this->subtaskModel->getStatusList(), 'subtask' => $subtask, 'task' => $task, ))); @@ -97,16 +108,16 @@ class Subtask extends Base list($valid, $errors) = $this->subtaskValidator->validateModification($values); if ($valid) { - if ($this->subtask->update($values)) { + if ($this->subtaskModel->update($values)) { $this->flash->success(t('Sub-task updated successfully.')); } else { $this->flash->failure(t('Unable to update your sub-task.')); } - return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** @@ -136,13 +147,13 @@ class Subtask extends Base $task = $this->getTask(); $subtask = $this->getSubtask(); - if ($this->subtask->remove($subtask['id'])) { + if ($this->subtaskModel->remove($subtask['id'])) { $this->flash->success(t('Sub-task removed successfully.')); } else { $this->flash->failure(t('Unable to remove this sub-task.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } /** @@ -156,11 +167,11 @@ class Subtask extends Base $task_id = $this->request->getIntegerParam('task_id'); $values = $this->request->getJson(); - if (! empty($values) && $this->helper->user->hasProjectAccess('Subtask', 'movePosition', $project_id)) { - $result = $this->subtask->changePosition($task_id, $values['subtask_id'], $values['position']); - return $this->response->json(array('result' => $result)); + if (! empty($values) && $this->helper->user->hasProjectAccess('SubtaskController', 'movePosition', $project_id)) { + $result = $this->subtaskModel->changePosition($task_id, $values['subtask_id'], $values['position']); + $this->response->json(array('result' => $result)); + } else { + throw new AccessForbiddenException(); } - - $this->forbidden(); } } diff --git a/sources/app/Controller/SubtaskConverterController.php b/sources/app/Controller/SubtaskConverterController.php new file mode 100644 index 0000000..65bcd2d --- /dev/null +++ b/sources/app/Controller/SubtaskConverterController.php @@ -0,0 +1,39 @@ +getTask(); + $subtask = $this->getSubtask(); + + $this->response->html($this->template->render('subtask_converter/show', array( + 'subtask' => $subtask, + 'task' => $task, + ))); + } + + public function save() + { + $project = $this->getProject(); + $subtask = $this->getSubtask(); + + $task_id = $this->subtaskModel->convertToTask($project['id'], $subtask['id']); + + if ($task_id !== false) { + $this->flash->success(t('Subtask converted to task successfully.')); + } else { + $this->flash->failure(t('Unable to convert the subtask.')); + } + + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $project['id'], 'task_id' => $task_id)), true); + } +} diff --git a/sources/app/Controller/SubtaskRestriction.php b/sources/app/Controller/SubtaskRestrictionController.php similarity index 66% rename from sources/app/Controller/SubtaskRestriction.php rename to sources/app/Controller/SubtaskRestrictionController.php index 5602486..084fc0d 100644 --- a/sources/app/Controller/SubtaskRestriction.php +++ b/sources/app/Controller/SubtaskRestrictionController.php @@ -2,32 +2,32 @@ namespace Kanboard\Controller; -use Kanboard\Model\Subtask as SubtaskModel; +use Kanboard\Model\SubtaskModel; /** * Subtask Restriction * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class SubtaskRestriction extends Base +class SubtaskRestrictionController extends BaseController { /** * Show popup * * @access public */ - public function popover() + public function show() { $task = $this->getTask(); $subtask = $this->getSubtask(); - $this->response->html($this->template->render('subtask_restriction/popover', array( + $this->response->html($this->template->render('subtask_restriction/show', array( 'status_list' => array( SubtaskModel::STATUS_TODO => t('Todo'), SubtaskModel::STATUS_DONE => t('Done'), ), - 'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()), + 'subtask_inprogress' => $this->subtaskModel->getSubtaskInProgress($this->userSession->getId()), 'subtask' => $subtask, 'task' => $task, ))); @@ -38,24 +38,24 @@ class SubtaskRestriction extends Base * * @access public */ - public function update() + public function save() { $task = $this->getTask(); $subtask = $this->getSubtask(); $values = $this->request->getValues(); // Change status of the previous "in progress" subtask - $this->subtask->update(array( + $this->subtaskModel->update(array( 'id' => $values['id'], 'status' => $values['status'], )); // Set the current subtask to "in progress" - $this->subtask->update(array( + $this->subtaskModel->update(array( 'id' => $subtask['id'], 'status' => SubtaskModel::STATUS_INPROGRESS, )); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } } diff --git a/sources/app/Controller/SubtaskStatus.php b/sources/app/Controller/SubtaskStatusController.php similarity index 73% rename from sources/app/Controller/SubtaskStatus.php rename to sources/app/Controller/SubtaskStatusController.php index 4fb82fc..699951f 100644 --- a/sources/app/Controller/SubtaskStatus.php +++ b/sources/app/Controller/SubtaskStatusController.php @@ -5,10 +5,10 @@ namespace Kanboard\Controller; /** * Subtask Status * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class SubtaskStatus extends Base +class SubtaskStatusController extends BaseController { /** * Change status to the next status: Toto -> In Progress -> Done @@ -20,7 +20,7 @@ class SubtaskStatus extends Base $task = $this->getTask(); $subtask = $this->getSubtask(); - $status = $this->subtask->toggleStatus($subtask['id']); + $status = $this->subtaskModel->toggleStatus($subtask['id']); if ($this->request->getIntegerParam('refresh-table') === 0) { $subtask['status'] = $status; @@ -44,10 +44,10 @@ class SubtaskStatus extends Base $timer = $this->request->getStringParam('timer'); if ($timer === 'start') { - $this->subtaskTimeTracking->logStartTime($subtask_id, $this->userSession->getId()); + $this->subtaskTimeTrackingModel->logStartTime($subtask_id, $this->userSession->getId()); } elseif ($timer === 'stop') { - $this->subtaskTimeTracking->logEndTime($subtask_id, $this->userSession->getId()); - $this->subtaskTimeTracking->updateTaskTimeTracking($task['id']); + $this->subtaskTimeTrackingModel->logEndTime($subtask_id, $this->userSession->getId()); + $this->subtaskTimeTrackingModel->updateTaskTimeTracking($task['id']); } $this->response->html($this->renderTable($task)); @@ -64,9 +64,8 @@ class SubtaskStatus extends Base { return $this->template->render('subtask/table', array( 'task' => $task, - 'subtasks' => $this->subtask->getAll($task['id']), + 'subtasks' => $this->subtaskModel->getAll($task['id']), 'editable' => true, - 'redirect' => 'task', )); } } diff --git a/sources/app/Controller/Swimlane.php b/sources/app/Controller/SwimlaneController.php similarity index 66% rename from sources/app/Controller/Swimlane.php rename to sources/app/Controller/SwimlaneController.php index 8270a16..c7c20ce 100644 --- a/sources/app/Controller/Swimlane.php +++ b/sources/app/Controller/SwimlaneController.php @@ -2,30 +2,31 @@ namespace Kanboard\Controller; -use Kanboard\Model\Swimlane as SwimlaneModel; +use Kanboard\Core\Controller\AccessForbiddenException; +use Kanboard\Core\Controller\PageNotFoundException; +use Kanboard\Model\SwimlaneModel; /** - * Swimlanes + * Swimlanes Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Swimlane extends Base +class SwimlaneController extends BaseController { /** * Get the swimlane (common method between actions) * * @access private - * @param integer $project_id * @return array + * @throws PageNotFoundException */ - private function getSwimlane($project_id) + private function getSwimlane() { - $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id')); + $swimlane = $this->swimlaneModel->getById($this->request->getIntegerParam('swimlane_id')); if (empty($swimlane)) { - $this->flash->failure(t('Swimlane not found.')); - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project_id))); + throw new PageNotFoundException(); } return $swimlane; @@ -41,9 +42,9 @@ class Swimlane extends Base $project = $this->getProject(); $this->response->html($this->helper->layout->project('swimlane/index', array( - 'default_swimlane' => $this->swimlane->getDefault($project['id']), - 'active_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::ACTIVE), - 'inactive_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::INACTIVE), + 'default_swimlane' => $this->swimlaneModel->getDefault($project['id']), + 'active_swimlanes' => $this->swimlaneModel->getAllByStatus($project['id'], SwimlaneModel::ACTIVE), + 'inactive_swimlanes' => $this->swimlaneModel->getAllByStatus($project['id'], SwimlaneModel::INACTIVE), 'project' => $project, 'title' => t('Swimlanes') ))); @@ -53,6 +54,9 @@ class Swimlane extends Base * Create a new swimlane * * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\PageNotFoundException */ public function create(array $values = array(), array $errors = array()) { @@ -77,26 +81,29 @@ class Swimlane extends Base list($valid, $errors) = $this->swimlaneValidator->validateCreation($values); if ($valid) { - if ($this->swimlane->create($values)) { + if ($this->swimlaneModel->create($values) !== false) { $this->flash->success(t('Your swimlane have been created successfully.')); - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + return $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id']))); } else { $errors = array('name' => array(t('Another swimlane with the same name exists in the project'))); } } - $this->create($values, $errors); + return $this->create($values, $errors); } /** * Edit default swimlane (display the form) * * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\PageNotFoundException */ public function editDefault(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $swimlane = $this->swimlane->getDefault($project['id']); + $swimlane = $this->swimlaneModel->getDefault($project['id']); $this->response->html($this->helper->layout->project('swimlane/edit_default', array( 'values' => empty($values) ? $swimlane : $values, @@ -118,26 +125,29 @@ class Swimlane extends Base list($valid, $errors) = $this->swimlaneValidator->validateDefaultModification($values); if ($valid) { - if ($this->swimlane->updateDefault($values)) { + if ($this->swimlaneModel->updateDefault($values)) { $this->flash->success(t('The default swimlane have been updated successfully.')); - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id'])), true); + return $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id'])), true); } else { $this->flash->failure(t('Unable to update this swimlane.')); } } - $this->editDefault($values, $errors); + return $this->editDefault($values, $errors); } /** * Edit a swimlane (display the form) * * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\PageNotFoundException */ public function edit(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $swimlane = $this->getSwimlane($project['id']); + $swimlane = $this->getSwimlane(); $this->response->html($this->helper->layout->project('swimlane/edit', array( 'values' => empty($values) ? $swimlane : $values, @@ -159,15 +169,15 @@ class Swimlane extends Base list($valid, $errors) = $this->swimlaneValidator->validateModification($values); if ($valid) { - if ($this->swimlane->update($values)) { + if ($this->swimlaneModel->update($values)) { $this->flash->success(t('Swimlane updated successfully.')); - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + return $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id']))); } else { $errors = array('name' => array(t('Another swimlane with the same name exists in the project'))); } } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** @@ -178,7 +188,7 @@ class Swimlane extends Base public function confirm() { $project = $this->getProject(); - $swimlane = $this->getSwimlane($project['id']); + $swimlane = $this->getSwimlane(); $this->response->html($this->helper->layout->project('swimlane/remove', array( 'project' => $project, @@ -197,13 +207,13 @@ class Swimlane extends Base $project = $this->getProject(); $swimlane_id = $this->request->getIntegerParam('swimlane_id'); - if ($this->swimlane->remove($project['id'], $swimlane_id)) { + if ($this->swimlaneModel->remove($project['id'], $swimlane_id)) { $this->flash->success(t('Swimlane removed successfully.')); } else { $this->flash->failure(t('Unable to remove this swimlane.')); } - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id']))); } /** @@ -217,13 +227,13 @@ class Swimlane extends Base $project = $this->getProject(); $swimlane_id = $this->request->getIntegerParam('swimlane_id'); - if ($this->swimlane->disable($project['id'], $swimlane_id)) { + if ($this->swimlaneModel->disable($project['id'], $swimlane_id)) { $this->flash->success(t('Swimlane updated successfully.')); } else { $this->flash->failure(t('Unable to update this swimlane.')); } - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id']))); } /** @@ -236,13 +246,13 @@ class Swimlane extends Base $this->checkCSRFParam(); $project = $this->getProject(); - if ($this->swimlane->disableDefault($project['id'])) { + if ($this->swimlaneModel->disableDefault($project['id'])) { $this->flash->success(t('Swimlane updated successfully.')); } else { $this->flash->failure(t('Unable to update this swimlane.')); } - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id']))); } /** @@ -256,13 +266,13 @@ class Swimlane extends Base $project = $this->getProject(); $swimlane_id = $this->request->getIntegerParam('swimlane_id'); - if ($this->swimlane->enable($project['id'], $swimlane_id)) { + if ($this->swimlaneModel->enable($project['id'], $swimlane_id)) { $this->flash->success(t('Swimlane updated successfully.')); } else { $this->flash->failure(t('Unable to update this swimlane.')); } - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id']))); } /** @@ -275,13 +285,13 @@ class Swimlane extends Base $this->checkCSRFParam(); $project = $this->getProject(); - if ($this->swimlane->enableDefault($project['id'])) { + if ($this->swimlaneModel->enableDefault($project['id'])) { $this->flash->success(t('Swimlane updated successfully.')); } else { $this->flash->failure(t('Unable to update this swimlane.')); } - $this->response->redirect($this->helper->url->to('swimlane', 'index', array('project_id' => $project['id']))); + $this->response->redirect($this->helper->url->to('SwimlaneController', 'index', array('project_id' => $project['id']))); } /** @@ -295,10 +305,10 @@ class Swimlane extends Base $values = $this->request->getJson(); if (! empty($values) && isset($values['swimlane_id']) && isset($values['position'])) { - $result = $this->swimlane->changePosition($project['id'], $values['swimlane_id'], $values['position']); - return $this->response->json(array('result' => $result)); + $result = $this->swimlaneModel->changePosition($project['id'], $values['swimlane_id'], $values['position']); + $this->response->json(array('result' => $result)); + } else { + throw new AccessForbiddenException(); } - - $this->forbidden(); } } diff --git a/sources/app/Controller/TagController.php b/sources/app/Controller/TagController.php new file mode 100644 index 0000000..b838991 --- /dev/null +++ b/sources/app/Controller/TagController.php @@ -0,0 +1,121 @@ +response->html($this->helper->layout->config('tag/index', array( + 'tags' => $this->tagModel->getAllByProject(0), + 'title' => t('Settings').' > '.t('Global tags management'), + ))); + } + + public function create(array $values = array(), array $errors = array()) + { + if (empty($values)) { + $values['project_id'] = 0; + } + + $this->response->html($this->template->render('tag/create', array( + 'values' => $values, + 'errors' => $errors, + ))); + } + + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->tagValidator->validateCreation($values); + + if ($valid) { + if ($this->tagModel->create(0, $values['name']) > 0) { + $this->flash->success(t('Tag created successfully.')); + } else { + $this->flash->failure(t('Unable to create this tag.')); + } + + $this->response->redirect($this->helper->url->to('TagController', 'index')); + } else { + $this->create($values, $errors); + } + } + + public function edit(array $values = array(), array $errors = array()) + { + $tag_id = $this->request->getIntegerParam('tag_id'); + $tag = $this->tagModel->getById($tag_id); + + if (empty($values)) { + $values = $tag; + } + + $this->response->html($this->template->render('tag/edit', array( + 'tag' => $tag, + 'values' => $values, + 'errors' => $errors, + ))); + } + + public function update() + { + $tag_id = $this->request->getIntegerParam('tag_id'); + $tag = $this->tagModel->getById($tag_id); + $values = $this->request->getValues(); + list($valid, $errors) = $this->tagValidator->validateModification($values); + + if ($tag['project_id'] != 0) { + throw new AccessForbiddenException(); + } + + if ($valid) { + if ($this->tagModel->update($values['id'], $values['name'])) { + $this->flash->success(t('Tag updated successfully.')); + } else { + $this->flash->failure(t('Unable to update this tag.')); + } + + $this->response->redirect($this->helper->url->to('TagController', 'index')); + } else { + $this->edit($values, $errors); + } + } + + public function confirm() + { + $tag_id = $this->request->getIntegerParam('tag_id'); + $tag = $this->tagModel->getById($tag_id); + + $this->response->html($this->template->render('tag/remove', array( + 'tag' => $tag, + ))); + } + + public function remove() + { + $this->checkCSRFParam(); + $tag_id = $this->request->getIntegerParam('tag_id'); + $tag = $this->tagModel->getById($tag_id); + + if ($tag['project_id'] != 0) { + throw new AccessForbiddenException(); + } + + if ($this->tagModel->remove($tag_id)) { + $this->flash->success(t('Tag removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this tag.')); + } + + $this->response->redirect($this->helper->url->to('TagController', 'index')); + } +} diff --git a/sources/app/Controller/Task.php b/sources/app/Controller/Task.php deleted file mode 100644 index 902a32d..0000000 --- a/sources/app/Controller/Task.php +++ /dev/null @@ -1,174 +0,0 @@ -project->getByToken($this->request->getStringParam('token')); - - // Token verification - if (empty($project)) { - return $this->forbidden(true); - } - - $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); - - if (empty($task)) { - return $this->notfound(true); - } - - if ($task['project_id'] != $project['id']) { - return $this->forbidden(true); - } - - $this->response->html($this->helper->layout->app('task/public', array( - 'project' => $project, - 'comments' => $this->comment->getAll($task['id']), - 'subtasks' => $this->subtask->getAll($task['id']), - 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), - 'task' => $task, - 'columns_list' => $this->column->getList($task['project_id']), - 'colors_list' => $this->color->getList(), - 'title' => $task['title'], - 'no_layout' => true, - 'auto_refresh' => true, - 'not_editable' => true, - ))); - } - - /** - * Show a task - * - * @access public - */ - public function show() - { - $task = $this->getTask(); - $subtasks = $this->subtask->getAll($task['id']); - - $values = array( - 'id' => $task['id'], - 'date_started' => $task['date_started'], - 'time_estimated' => $task['time_estimated'] ?: '', - 'time_spent' => $task['time_spent'] ?: '', - ); - - $values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT)); - - $this->response->html($this->helper->layout->task('task/show', array( - 'task' => $task, - 'project' => $this->project->getById($task['project_id']), - 'values' => $values, - 'files' => $this->taskFile->getAllDocuments($task['id']), - 'images' => $this->taskFile->getAllImages($task['id']), - 'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting()), - 'subtasks' => $subtasks, - 'internal_links' => $this->taskLink->getAllGroupedByLabel($task['id']), - 'external_links' => $this->taskExternalLink->getAll($task['id']), - 'link_label_list' => $this->link->getList(0, false), - ))); - } - - /** - * Display task analytics - * - * @access public - */ - public function analytics() - { - $task = $this->getTask(); - - $this->response->html($this->helper->layout->task('task/analytics', array( - 'task' => $task, - 'project' => $this->project->getById($task['project_id']), - 'lead_time' => $this->taskAnalytic->getLeadTime($task), - 'cycle_time' => $this->taskAnalytic->getCycleTime($task), - 'time_spent_columns' => $this->taskAnalytic->getTimeSpentByColumn($task), - ))); - } - - /** - * Display the time tracking details - * - * @access public - */ - public function timetracking() - { - $task = $this->getTask(); - - $subtask_paginator = $this->paginator - ->setUrl('task', 'timetracking', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks')) - ->setMax(15) - ->setOrder('start') - ->setDirection('DESC') - ->setQuery($this->subtaskTimeTracking->getTaskQuery($task['id'])) - ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); - - $this->response->html($this->helper->layout->task('task/time_tracking_details', array( - 'task' => $task, - 'project' => $this->project->getById($task['project_id']), - 'subtask_paginator' => $subtask_paginator, - ))); - } - - /** - * Display the task transitions - * - * @access public - */ - public function transitions() - { - $task = $this->getTask(); - - $this->response->html($this->helper->layout->task('task/transitions', array( - 'task' => $task, - 'project' => $this->project->getById($task['project_id']), - 'transitions' => $this->transition->getAllByTask($task['id']), - ))); - } - - /** - * Remove a task - * - * @access public - */ - public function remove() - { - $task = $this->getTask(); - - if (! $this->taskPermission->canRemoveTask($task)) { - $this->forbidden(); - } - - if ($this->request->getStringParam('confirmation') === 'yes') { - $this->checkCSRFParam(); - - if ($this->task->remove($task['id'])) { - $this->flash->success(t('Task removed successfully.')); - } else { - $this->flash->failure(t('Unable to remove this task.')); - } - - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); - } - - $this->response->html($this->template->render('task/remove', array( - 'task' => $task, - ))); - } -} diff --git a/sources/app/Controller/TaskAjaxController.php b/sources/app/Controller/TaskAjaxController.php new file mode 100644 index 0000000..f9feff1 --- /dev/null +++ b/sources/app/Controller/TaskAjaxController.php @@ -0,0 +1,49 @@ +request->getStringParam('term'); + $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId()); + $exclude_task_id = $this->request->getIntegerParam('exclude_task_id'); + + if (empty($project_ids)) { + $this->response->json(array()); + } else { + + $filter = $this->taskQuery->withFilter(new TaskProjectsFilter($project_ids)); + + if (! empty($exclude_task_id)) { + $filter->withFilter(new TaskIdExclusionFilter(array($exclude_task_id))); + } + + if (ctype_digit($search)) { + $filter->withFilter(new TaskIdFilter($search)); + } else { + $filter->withFilter(new TaskTitleFilter($search)); + } + + $this->response->json($filter->format(new TaskAutoCompleteFormatter($this->container))); + } + } +} diff --git a/sources/app/Controller/TaskBulkController.php b/sources/app/Controller/TaskBulkController.php new file mode 100644 index 0000000..df7f589 --- /dev/null +++ b/sources/app/Controller/TaskBulkController.php @@ -0,0 +1,89 @@ +getProject(); + + if (empty($values)) { + $values = array( + 'swimlane_id' => $this->request->getIntegerParam('swimlane_id'), + 'column_id' => $this->request->getIntegerParam('column_id'), + 'project_id' => $project['id'], + ); + } + + $this->response->html($this->template->render('task_bulk/show', array( + 'project' => $project, + 'values' => $values, + 'errors' => $errors, + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id'], true, false, true), + 'colors_list' => $this->colorModel->getList(), + 'categories_list' => $this->categoryModel->getList($project['id']), + ))); + } + + /** + * Save all tasks in the database + */ + public function save() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + list($valid, $errors) = $this->taskValidator->validateBulkCreation($values); + + if ($valid) { + $this->createTasks($project, $values); + $this->response->redirect($this->helper->url->to( + 'BoardViewController', + 'show', + array('project_id' => $project['id']), + 'swimlane-'. $values['swimlane_id'] + ), true); + } else { + $this->show($values, $errors); + } + } + + /** + * Create all tasks + * + * @param array $project + * @param array $values + */ + protected function createTasks(array $project, array $values) + { + $tasks = preg_split('/\r\n|[\r\n]/', $values['tasks']); + + foreach ($tasks as $title) { + $title = trim($title); + + if (! empty($title)) { + $this->taskCreationModel->create(array( + 'title' => $title, + 'column_id' => $values['column_id'], + 'swimlane_id' => $values['swimlane_id'], + 'category_id' => empty($values['category_id']) ? 0 : $values['category_id'], + 'owner_id' => empty($values['owner_id']) ? 0 : $values['owner_id'], + 'color_id' => $values['color_id'], + 'project_id' => $project['id'], + )); + } + } + } +} diff --git a/sources/app/Controller/Taskcreation.php b/sources/app/Controller/TaskCreationController.php similarity index 67% rename from sources/app/Controller/Taskcreation.php rename to sources/app/Controller/TaskCreationController.php index 1d8a0e2..073b31b 100644 --- a/sources/app/Controller/Taskcreation.php +++ b/sources/app/Controller/TaskCreationController.php @@ -3,28 +3,31 @@ namespace Kanboard\Controller; /** - * Task Creation controller + * Task Creation Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Taskcreation extends Base +class TaskCreationController extends BaseController { /** * Display a form to create a new task * * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\PageNotFoundException */ - public function create(array $values = array(), array $errors = array()) + public function show(array $values = array(), array $errors = array()) { $project = $this->getProject(); - $swimlanes_list = $this->swimlane->getList($project['id'], false, true); + $swimlanes_list = $this->swimlaneModel->getList($project['id'], false, true); if (empty($values)) { $values = array( 'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanes_list)), 'column_id' => $this->request->getIntegerParam('column_id'), - 'color_id' => $this->color->getDefaultColor(), + 'color_id' => $this->colorModel->getDefaultColor(), 'owner_id' => $this->userSession->getId(), ); @@ -32,14 +35,13 @@ class Taskcreation extends Base $values = $this->hook->merge('controller:task-creation:form:default', $values, array('default_values' => $values)); } - $this->response->html($this->template->render('task_creation/form', array( + $this->response->html($this->template->render('task_creation/show', array( 'project' => $project, 'errors' => $errors, 'values' => $values + array('project_id' => $project['id']), - 'columns_list' => $this->column->getList($project['id']), - 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], true, false, true), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($project['id']), + 'columns_list' => $this->columnModel->getList($project['id']), + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id'], true, false, true), + 'categories_list' => $this->categoryModel->getList($project['id']), 'swimlanes_list' => $swimlanes_list, 'title' => $project['name'].' > '.t('New task') ))); @@ -57,19 +59,19 @@ class Taskcreation extends Base list($valid, $errors) = $this->taskValidator->validateCreation($values); - if ($valid && $this->taskCreation->create($values)) { + if ($valid && $this->taskCreationModel->create($values)) { $this->flash->success(t('Task created successfully.')); return $this->afterSave($project, $values); } $this->flash->failure(t('Unable to create your task.')); - $this->create($values, $errors); + return $this->show($values, $errors); } private function afterSave(array $project, array &$values) { if (isset($values['another_task']) && $values['another_task'] == 1) { - return $this->create(array( + return $this->show(array( 'owner_id' => $values['owner_id'], 'color_id' => $values['color_id'], 'category_id' => isset($values['category_id']) ? $values['category_id'] : 0, @@ -79,6 +81,6 @@ class Taskcreation extends Base )); } - $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id']))); + return $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true); } } diff --git a/sources/app/Controller/Taskduplication.php b/sources/app/Controller/TaskDuplicationController.php similarity index 65% rename from sources/app/Controller/Taskduplication.php rename to sources/app/Controller/TaskDuplicationController.php index 8fca930..915bf8f 100644 --- a/sources/app/Controller/Taskduplication.php +++ b/sources/app/Controller/TaskDuplicationController.php @@ -5,10 +5,10 @@ namespace Kanboard\Controller; /** * Task Duplication controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Taskduplication extends Base +class TaskDuplicationController extends BaseController { /** * Duplicate a task @@ -21,18 +21,18 @@ class Taskduplication extends Base if ($this->request->getStringParam('confirmation') === 'yes') { $this->checkCSRFParam(); - $task_id = $this->taskDuplication->duplicate($task['id']); + $task_id = $this->taskDuplicationModel->duplicate($task['id']); if ($task_id > 0) { $this->flash->success(t('Task created successfully.')); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task_id))); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task_id))); } else { $this->flash->failure(t('Unable to create this task.')); - $this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); + return $this->response->redirect($this->helper->url->to('TaskDuplicationController', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } } - $this->response->html($this->template->render('task_duplication/duplicate', array( + return $this->response->html($this->template->render('task_duplication/duplicate', array( 'task' => $task, ))); } @@ -50,20 +50,20 @@ class Taskduplication extends Base $values = $this->request->getValues(); list($valid, ) = $this->taskValidator->validateProjectModification($values); - if ($valid && $this->taskDuplication->moveToProject($task['id'], + if ($valid && $this->taskProjectMoveModel->moveToProject($task['id'], $values['project_id'], $values['swimlane_id'], $values['column_id'], $values['category_id'], $values['owner_id'])) { $this->flash->success(t('Task updated successfully.')); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $values['project_id'], 'task_id' => $task['id']))); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $values['project_id'], 'task_id' => $task['id']))); } $this->flash->failure(t('Unable to update your task.')); } - $this->chooseDestination($task, 'task_duplication/move'); + return $this->chooseDestination($task, 'task_duplication/move'); } /** @@ -80,21 +80,21 @@ class Taskduplication extends Base list($valid, ) = $this->taskValidator->validateProjectModification($values); if ($valid) { - $task_id = $this->taskDuplication->duplicateToProject( + $task_id = $this->taskProjectDuplicationModel->duplicateToProject( $task['id'], $values['project_id'], $values['swimlane_id'], $values['column_id'], $values['category_id'], $values['owner_id'] ); if ($task_id > 0) { $this->flash->success(t('Task created successfully.')); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $values['project_id'], 'task_id' => $task_id))); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $values['project_id'], 'task_id' => $task_id))); } } $this->flash->failure(t('Unable to create your task.')); } - $this->chooseDestination($task, 'task_duplication/copy'); + return $this->chooseDestination($task, 'task_duplication/copy'); } /** @@ -107,19 +107,19 @@ class Taskduplication extends Base private function chooseDestination(array $task, $template) { $values = array(); - $projects_list = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); + $projects_list = $this->projectUserRoleModel->getActiveProjectsByUser($this->userSession->getId()); unset($projects_list[$task['project_id']]); if (! empty($projects_list)) { $dst_project_id = $this->request->getIntegerParam('dst_project_id', key($projects_list)); - $swimlanes_list = $this->swimlane->getList($dst_project_id, false, true); - $columns_list = $this->column->getList($dst_project_id); - $categories_list = $this->category->getList($dst_project_id); - $users_list = $this->projectUserRole->getAssignableUsersList($dst_project_id); + $swimlanes_list = $this->swimlaneModel->getList($dst_project_id, false, true); + $columns_list = $this->columnModel->getList($dst_project_id); + $categories_list = $this->categoryModel->getList($dst_project_id); + $users_list = $this->projectUserRoleModel->getAssignableUsersList($dst_project_id); - $values = $this->taskDuplication->checkDestinationProjectValues($task); + $values = $this->taskDuplicationModel->checkDestinationProjectValues($task); $values['project_id'] = $dst_project_id; } else { $swimlanes_list = array(); diff --git a/sources/app/Controller/TaskExternalLink.php b/sources/app/Controller/TaskExternalLinkController.php similarity index 71% rename from sources/app/Controller/TaskExternalLink.php rename to sources/app/Controller/TaskExternalLinkController.php index 0db8ec3..9c04eb0 100644 --- a/sources/app/Controller/TaskExternalLink.php +++ b/sources/app/Controller/TaskExternalLinkController.php @@ -2,20 +2,25 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\PageNotFoundException; use Kanboard\Core\ExternalLink\ExternalLinkProviderNotFound; /** * Task External Link Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class TaskExternalLink extends Base +class TaskExternalLinkController extends BaseController { /** * First creation form * * @access public + * @param array $values + * @param array $errors + * @throws PageNotFoundException + * @throws \Kanboard\Core\Controller\AccessForbiddenException */ public function find(array $values = array(), array $errors = array()) { @@ -36,11 +41,10 @@ class TaskExternalLink extends Base */ public function create() { + $task = $this->getTask(); + $values = $this->request->getValues(); + try { - - $task = $this->getTask(); - $values = $this->request->getValues(); - $provider = $this->externalLinkManager->setUserInput($values)->find(); $link = $provider->getLink(); @@ -72,18 +76,23 @@ class TaskExternalLink extends Base $values = $this->request->getValues(); list($valid, $errors) = $this->externalLinkValidator->validateCreation($values); - if ($valid && $this->taskExternalLink->create($values)) { + if ($valid && $this->taskExternalLinkModel->create($values) !== false) { $this->flash->success(t('Link added successfully.')); - return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** * Edit form * * @access public + * @param array $values + * @param array $errors + * @throws ExternalLinkProviderNotFound + * @throws PageNotFoundException + * @throws \Kanboard\Core\Controller\AccessForbiddenException */ public function edit(array $values = array(), array $errors = array()) { @@ -91,11 +100,11 @@ class TaskExternalLink extends Base $link_id = $this->request->getIntegerParam('link_id'); if ($link_id > 0) { - $values = $this->taskExternalLink->getById($link_id); + $values = $this->taskExternalLinkModel->getById($link_id); } if (empty($values)) { - return $this->notfound(); + throw new PageNotFoundException(); } $provider = $this->externalLinkManager->getProvider($values['link_type']); @@ -119,12 +128,12 @@ class TaskExternalLink extends Base $values = $this->request->getValues(); list($valid, $errors) = $this->externalLinkValidator->validateModification($values); - if ($valid && $this->taskExternalLink->update($values)) { + if ($valid && $this->taskExternalLinkModel->update($values)) { $this->flash->success(t('Link updated successfully.')); - return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** @@ -136,10 +145,10 @@ class TaskExternalLink extends Base { $task = $this->getTask(); $link_id = $this->request->getIntegerParam('link_id'); - $link = $this->taskExternalLink->getById($link_id); + $link = $this->taskExternalLinkModel->getById($link_id); if (empty($link)) { - return $this->notfound(); + throw new PageNotFoundException(); } $this->response->html($this->template->render('task_external_link/remove', array( @@ -158,12 +167,12 @@ class TaskExternalLink extends Base $this->checkCSRFParam(); $task = $this->getTask(); - if ($this->taskExternalLink->remove($this->request->getIntegerParam('link_id'))) { + if ($this->taskExternalLinkModel->remove($this->request->getIntegerParam('link_id'))) { $this->flash->success(t('Link removed successfully.')); } else { $this->flash->failure(t('Unable to remove this link.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } } diff --git a/sources/app/Controller/TaskFile.php b/sources/app/Controller/TaskFileController.php similarity index 60% rename from sources/app/Controller/TaskFile.php rename to sources/app/Controller/TaskFileController.php index 2b0152a..77c0c02 100644 --- a/sources/app/Controller/TaskFile.php +++ b/sources/app/Controller/TaskFileController.php @@ -5,10 +5,10 @@ namespace Kanboard\Controller; /** * Task File Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class TaskFile extends Base +class TaskFileController extends BaseController { /** * Screenshot @@ -19,12 +19,12 @@ class TaskFile extends Base { $task = $this->getTask(); - if ($this->request->isPost() && $this->taskFile->uploadScreenshot($task['id'], $this->request->getValue('screenshot')) !== false) { + if ($this->request->isPost() && $this->taskFileModel->uploadScreenshot($task['id'], $this->request->getValue('screenshot')) !== false) { $this->flash->success(t('Screenshot uploaded successfully.')); - return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); } - $this->response->html($this->template->render('task_file/screenshot', array( + return $this->response->html($this->template->render('task_file/screenshot', array( 'task' => $task, ))); } @@ -53,11 +53,11 @@ class TaskFile extends Base { $task = $this->getTask(); - if (! $this->taskFile->uploadFiles($task['id'], $this->request->getFileInfo('files'))) { + if (! $this->taskFileModel->uploadFiles($task['id'], $this->request->getFileInfo('files'))) { $this->flash->failure(t('Unable to upload the file.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); } /** @@ -69,15 +69,15 @@ class TaskFile extends Base { $this->checkCSRFParam(); $task = $this->getTask(); - $file = $this->taskFile->getById($this->request->getIntegerParam('file_id')); + $file = $this->taskFileModel->getById($this->request->getIntegerParam('file_id')); - if ($file['task_id'] == $task['id'] && $this->taskFile->remove($file['id'])) { + if ($file['task_id'] == $task['id'] && $this->taskFileModel->remove($file['id'])) { $this->flash->success(t('File removed successfully.')); } else { $this->flash->failure(t('Unable to remove this file.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } /** @@ -88,7 +88,7 @@ class TaskFile extends Base public function confirm() { $task = $this->getTask(); - $file = $this->taskFile->getById($this->request->getIntegerParam('file_id')); + $file = $this->taskFileModel->getById($this->request->getIntegerParam('file_id')); $this->response->html($this->template->render('task_file/remove', array( 'task' => $task, diff --git a/sources/app/Controller/TaskGanttController.php b/sources/app/Controller/TaskGanttController.php new file mode 100644 index 0000000..868368e --- /dev/null +++ b/sources/app/Controller/TaskGanttController.php @@ -0,0 +1,62 @@ +getProject(); + $search = $this->helper->projectHeader->getSearchQuery($project); + $sorting = $this->request->getStringParam('sorting', 'board'); + $filter = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project['id'])); + + if ($sorting === 'date') { + $filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation'); + } else { + $filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position'); + } + + $this->response->html($this->helper->layout->app('task_gantt/show', array( + 'project' => $project, + 'title' => $project['name'], + 'description' => $this->helper->projectHeader->getDescription($project), + 'sorting' => $sorting, + 'tasks' => $filter->format(new TaskGanttFormatter($this->container)), + ))); + } + + /** + * Save new task start date and due date + */ + public function save() + { + $this->getProject(); + $values = $this->request->getJson(); + + $result = $this->taskModificationModel->update(array( + 'id' => $values['id'], + 'date_started' => strtotime($values['start']), + 'date_due' => strtotime($values['end']), + )); + + if (! $result) { + $this->response->json(array('message' => 'Unable to save task'), 400); + } else { + $this->response->json(array('message' => 'OK'), 201); + } + } +} diff --git a/sources/app/Controller/TaskGanttCreationController.php b/sources/app/Controller/TaskGanttCreationController.php new file mode 100644 index 0000000..07b74a4 --- /dev/null +++ b/sources/app/Controller/TaskGanttCreationController.php @@ -0,0 +1,70 @@ +getProject(); + + $values = $values + array( + 'project_id' => $project['id'], + 'column_id' => $this->columnModel->getFirstColumnId($project['id']), + 'position' => 1 + ); + + $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); + $values = $this->hook->merge('controller:gantt:task:form:default', $values, array('default_values' => $values)); + + $this->response->html($this->template->render('task_gantt_creation/show', array( + 'project' => $project, + 'errors' => $errors, + 'values' => $values, + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id'], true, false, true), + 'categories_list' => $this->categoryModel->getList($project['id']), + 'swimlanes_list' => $this->swimlaneModel->getList($project['id'], false, true), + 'title' => $project['name'].' > '.t('New task') + ))); + } + + /** + * Validate and save a new task + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateCreation($values); + + if ($valid) { + $task_id = $this->taskCreationModel->create($values); + + if ($task_id !== false) { + $this->flash->success(t('Task created successfully.')); + return $this->response->redirect($this->helper->url->to('TaskGanttController', 'show', array('project_id' => $project['id']))); + } else { + $this->flash->failure(t('Unable to create your task.')); + } + } + + return $this->show($values, $errors); + } +} diff --git a/sources/app/Controller/TaskHelper.php b/sources/app/Controller/TaskHelper.php deleted file mode 100644 index 7e340a6..0000000 --- a/sources/app/Controller/TaskHelper.php +++ /dev/null @@ -1,41 +0,0 @@ -request->getStringParam('term'); - $projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); - - if (empty($projects)) { - $this->response->json(array()); - } - - $filter = $this->taskFilterAutoCompleteFormatter - ->create() - ->filterByProjects($projects) - ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id'))); - - // Search by task id or by title - if (ctype_digit($search)) { - $filter->filterById($search); - } else { - $filter->filterByTitle($search); - } - - $this->response->json($filter->format()); - } -} diff --git a/sources/app/Controller/TaskImport.php b/sources/app/Controller/TaskImport.php deleted file mode 100644 index 460c608..0000000 --- a/sources/app/Controller/TaskImport.php +++ /dev/null @@ -1,72 +0,0 @@ -getProject(); - - $this->response->html($this->helper->layout->project('task_import/step1', array( - 'project' => $project, - 'values' => $values, - 'errors' => $errors, - 'max_size' => ini_get('upload_max_filesize'), - 'delimiters' => Csv::getDelimiters(), - 'enclosures' => Csv::getEnclosures(), - 'title' => t('Import tasks from CSV file'), - ))); - } - - /** - * Process CSV file - * - */ - public function step2() - { - $project = $this->getProject(); - $values = $this->request->getValues(); - $filename = $this->request->getFilePath('file'); - - if (! file_exists($filename)) { - $this->step1($values, array('file' => array(t('Unable to read your file')))); - } - - $this->taskImport->projectId = $project['id']; - - $csv = new Csv($values['delimiter'], $values['enclosure']); - $csv->setColumnMapping($this->taskImport->getColumnMapping()); - $csv->read($filename, array($this->taskImport, 'import')); - - if ($this->taskImport->counter > 0) { - $this->flash->success(t('%d task(s) have been imported successfully.', $this->taskImport->counter)); - } else { - $this->flash->failure(t('Nothing have been imported!')); - } - - $this->response->redirect($this->helper->url->to('taskImport', 'step1', array('project_id' => $project['id']))); - } - - /** - * Generate template - * - */ - public function template() - { - $this->response->forceDownload('tasks.csv'); - $this->response->csv(array($this->taskImport->getColumnMapping())); - } -} diff --git a/sources/app/Controller/TaskImportController.php b/sources/app/Controller/TaskImportController.php new file mode 100644 index 0000000..aff2d39 --- /dev/null +++ b/sources/app/Controller/TaskImportController.php @@ -0,0 +1,74 @@ +getProject(); + + $this->response->html($this->helper->layout->project('task_import/show', array( + 'project' => $project, + 'values' => $values, + 'errors' => $errors, + 'max_size' => ini_get('upload_max_filesize'), + 'delimiters' => Csv::getDelimiters(), + 'enclosures' => Csv::getEnclosures(), + 'title' => t('Import tasks from CSV file'), + ), 'task_import/sidebar')); + } + + /** + * Process CSV file + */ + public function save() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + $filename = $this->request->getFilePath('file'); + + if (! file_exists($filename)) { + $this->show($values, array('file' => array(t('Unable to read your file')))); + } else { + $this->taskImport->projectId = $project['id']; + + $csv = new Csv($values['delimiter'], $values['enclosure']); + $csv->setColumnMapping($this->taskImport->getColumnMapping()); + $csv->read($filename, array($this->taskImport, 'import')); + + if ($this->taskImport->counter > 0) { + $this->flash->success(t('%d task(s) have been imported successfully.', $this->taskImport->counter)); + } else { + $this->flash->failure(t('Nothing have been imported!')); + } + + $this->response->redirect($this->helper->url->to('TaskImportController', 'show', array('project_id' => $project['id']))); + } + } + + /** + * Generate template + * + */ + public function template() + { + $this->response->withFileDownload('tasks.csv'); + $this->response->csv(array($this->taskImport->getColumnMapping())); + } +} diff --git a/sources/app/Controller/TaskInternalLink.php b/sources/app/Controller/TaskInternalLinkController.php similarity index 65% rename from sources/app/Controller/TaskInternalLink.php rename to sources/app/Controller/TaskInternalLinkController.php index ac5e04b..a140f1f 100644 --- a/sources/app/Controller/TaskInternalLink.php +++ b/sources/app/Controller/TaskInternalLinkController.php @@ -2,27 +2,30 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\PageNotFoundException; + /** * TaskInternalLink Controller * - * @package controller + * @package Kanboard\Controller * @author Olivier Maridat * @author Frederic Guillot */ -class TaskInternalLink extends Base +class TaskInternalLinkController extends BaseController { /** * Get the current link * * @access private * @return array + * @throws PageNotFoundException */ private function getTaskLink() { - $link = $this->taskLink->getById($this->request->getIntegerParam('link_id')); + $link = $this->taskLinkModel->getById($this->request->getIntegerParam('link_id')); if (empty($link)) { - return $this->notfound(); + throw new PageNotFoundException(); } return $link; @@ -32,6 +35,10 @@ class TaskInternalLink extends Base * Creation form * * @access public + * @param array $values + * @param array $errors + * @throws PageNotFoundException + * @throws \Kanboard\Core\Controller\AccessForbiddenException */ public function create(array $values = array(), array $errors = array()) { @@ -41,7 +48,7 @@ class TaskInternalLink extends Base 'values' => $values, 'errors' => $errors, 'task' => $task, - 'labels' => $this->link->getList(0, false), + 'labels' => $this->linkModel->getList(0, false), ))); } @@ -58,22 +65,26 @@ class TaskInternalLink extends Base list($valid, $errors) = $this->taskLinkValidator->validateCreation($values); if ($valid) { - if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) { + if ($this->taskLinkModel->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) { $this->flash->success(t('Link added successfully.')); - return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); } $errors = array('title' => array(t('The exact same link already exists'))); $this->flash->failure(t('Unable to create your link.')); } - $this->create($values, $errors); + return $this->create($values, $errors); } /** * Edit form * * @access public + * @param array $values + * @param array $errors + * @throws PageNotFoundException + * @throws \Kanboard\Core\Controller\AccessForbiddenException */ public function edit(array $values = array(), array $errors = array()) { @@ -81,7 +92,7 @@ class TaskInternalLink extends Base $task_link = $this->getTaskLink(); if (empty($values)) { - $opposite_task = $this->taskFinder->getById($task_link['opposite_task_id']); + $opposite_task = $this->taskFinderModel->getById($task_link['opposite_task_id']); $values = $task_link; $values['title'] = '#'.$opposite_task['id'].' - '.$opposite_task['title']; } @@ -91,7 +102,7 @@ class TaskInternalLink extends Base 'errors' => $errors, 'task_link' => $task_link, 'task' => $task, - 'labels' => $this->link->getList(0, false) + 'labels' => $this->linkModel->getList(0, false) ))); } @@ -108,15 +119,15 @@ class TaskInternalLink extends Base list($valid, $errors) = $this->taskLinkValidator->validateModification($values); if ($valid) { - if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) { + if ($this->taskLinkModel->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) { $this->flash->success(t('Link updated successfully.')); - return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); } $this->flash->failure(t('Unable to update your link.')); } - $this->edit($values, $errors); + return $this->edit($values, $errors); } /** @@ -145,12 +156,12 @@ class TaskInternalLink extends Base $this->checkCSRFParam(); $task = $this->getTask(); - if ($this->taskLink->remove($this->request->getIntegerParam('link_id'))) { + if ($this->taskLinkModel->remove($this->request->getIntegerParam('link_id'))) { $this->flash->success(t('Link removed successfully.')); } else { $this->flash->failure(t('Unable to remove this link.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } } diff --git a/sources/app/Controller/Listing.php b/sources/app/Controller/TaskListController.php similarity index 57% rename from sources/app/Controller/Listing.php rename to sources/app/Controller/TaskListController.php index 9931c34..c6d1fa9 100644 --- a/sources/app/Controller/Listing.php +++ b/sources/app/Controller/TaskListController.php @@ -2,15 +2,16 @@ namespace Kanboard\Controller; -use Kanboard\Model\Task as TaskModel; +use Kanboard\Filter\TaskProjectFilter; +use Kanboard\Model\TaskModel; /** - * List view controller + * Task List Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Listing extends Base +class TaskListController extends BaseController { /** * Show list view for projects @@ -21,17 +22,20 @@ class Listing extends Base { $project = $this->getProject(); $search = $this->helper->projectHeader->getSearchQuery($project); - $query = $this->taskFilter->search($search)->filterByProject($project['id'])->getQuery(); $paginator = $this->paginator - ->setUrl('listing', 'show', array('project_id' => $project['id'])) + ->setUrl('TaskListController', 'show', array('project_id' => $project['id'])) ->setMax(30) ->setOrder(TaskModel::TABLE.'.id') ->setDirection('DESC') - ->setQuery($query) + ->setQuery($this->taskLexer + ->build($search) + ->withFilter(new TaskProjectFilter($project['id'])) + ->getQuery() + ) ->calculate(); - $this->response->html($this->helper->layout->app('listing/show', array( + $this->response->html($this->helper->layout->app('task_list/show', array( 'project' => $project, 'title' => $project['name'], 'description' => $this->helper->projectHeader->getDescription($project), diff --git a/sources/app/Controller/TaskModificationController.php b/sources/app/Controller/TaskModificationController.php new file mode 100644 index 0000000..b064123 --- /dev/null +++ b/sources/app/Controller/TaskModificationController.php @@ -0,0 +1,78 @@ +getTask(); + $this->taskModificationModel->update(array('id' => $task['id'], 'date_started' => time())); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + + /** + * Display a form to edit a task + * + * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\AccessForbiddenException + * @throws \Kanboard\Core\Controller\PageNotFoundException + */ + public function edit(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + $project = $this->projectModel->getById($task['project_id']); + + if (empty($values)) { + $values = $task; + $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); + $values = $this->hook->merge('controller:task-modification:form:default', $values, array('default_values' => $values)); + $values = $this->dateParser->format($values, array('date_due'), $this->dateParser->getUserDateFormat()); + $values = $this->dateParser->format($values, array('date_started'), $this->dateParser->getUserDateTimeFormat()); + } + + $this->response->html($this->template->render('task_modification/show', array( + 'project' => $project, + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'tags' => $this->taskTagModel->getList($task['id']), + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($task['project_id']), + 'categories_list' => $this->categoryModel->getList($task['project_id']), + ))); + } + + /** + * Validate and update a task + * + * @access public + */ + public function update() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateModification($values); + + if ($valid && $this->taskModificationModel->update($values)) { + $this->flash->success(t('Task updated successfully.')); + $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); + } else { + $this->flash->failure(t('Unable to update your task.')); + $this->edit($values, $errors); + } + } +} diff --git a/sources/app/Controller/TaskPopoverController.php b/sources/app/Controller/TaskPopoverController.php new file mode 100644 index 0000000..4e3c11a --- /dev/null +++ b/sources/app/Controller/TaskPopoverController.php @@ -0,0 +1,26 @@ +getTask(); + + $this->response->html($this->template->render('task_file/screenshot', array( + 'task' => $task, + ))); + } +} diff --git a/sources/app/Controller/TaskRecurrence.php b/sources/app/Controller/TaskRecurrenceController.php similarity index 53% rename from sources/app/Controller/TaskRecurrence.php rename to sources/app/Controller/TaskRecurrenceController.php index 569ef8d..c6fdfa3 100644 --- a/sources/app/Controller/TaskRecurrence.php +++ b/sources/app/Controller/TaskRecurrenceController.php @@ -5,15 +5,19 @@ namespace Kanboard\Controller; /** * Task Recurrence controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class TaskRecurrence extends Base +class TaskRecurrenceController extends BaseController { /** * Edit recurrence form * * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\AccessForbiddenException + * @throws \Kanboard\Core\Controller\PageNotFoundException */ public function edit(array $values = array(), array $errors = array()) { @@ -27,10 +31,10 @@ class TaskRecurrence extends Base 'values' => $values, 'errors' => $errors, 'task' => $task, - 'recurrence_status_list' => $this->task->getRecurrenceStatusList(), - 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), - 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), - 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + 'recurrence_status_list' => $this->taskRecurrenceModel->getRecurrenceStatusList(), + 'recurrence_trigger_list' => $this->taskRecurrenceModel->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->taskRecurrenceModel->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->taskRecurrenceModel->getRecurrenceBasedateList(), ))); } @@ -47,15 +51,15 @@ class TaskRecurrence extends Base list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values); if ($valid) { - if ($this->taskModification->update($values)) { + if ($this->taskModificationModel->update($values)) { $this->flash->success(t('Task updated successfully.')); } else { $this->flash->failure(t('Unable to update your task.')); } - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); } - $this->edit($values, $errors); + return $this->edit($values, $errors); } } diff --git a/sources/app/Controller/Taskstatus.php b/sources/app/Controller/TaskStatusController.php similarity index 79% rename from sources/app/Controller/Taskstatus.php rename to sources/app/Controller/TaskStatusController.php index a67459c..82b4f9c 100644 --- a/sources/app/Controller/Taskstatus.php +++ b/sources/app/Controller/TaskStatusController.php @@ -5,10 +5,10 @@ namespace Kanboard\Controller; /** * Task Status controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class Taskstatus extends Base +class TaskStatusController extends BaseController { /** * Close a task @@ -46,16 +46,16 @@ class Taskstatus extends Base if ($this->request->getStringParam('confirmation') === 'yes') { $this->checkCSRFParam(); - if ($this->taskStatus->$method($task['id'])) { + if ($this->taskStatusModel->$method($task['id'])) { $this->flash->success($success_message); } else { $this->flash->failure($failure_message); } - return $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); + return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), true); } - $this->response->html($this->template->render($template, array( + return $this->response->html($this->template->render($template, array( 'task' => $task, ))); } diff --git a/sources/app/Controller/TaskSuppressionController.php b/sources/app/Controller/TaskSuppressionController.php new file mode 100644 index 0000000..600107c --- /dev/null +++ b/sources/app/Controller/TaskSuppressionController.php @@ -0,0 +1,53 @@ +getTask(); + + if (! $this->helper->user->canRemoveTask($task)) { + throw new AccessForbiddenException(); + } + + $this->response->html($this->template->render('task_suppression/remove', array( + 'task' => $task, + 'redirect' => $this->request->getStringParam('redirect'), + ))); + } + + /** + * Remove a task + */ + public function remove() + { + $task = $this->getTask(); + $this->checkCSRFParam(); + + if (! $this->helper->user->canRemoveTask($task)) { + throw new AccessForbiddenException(); + } + + if ($this->taskModel->remove($task['id'])) { + $this->flash->success(t('Task removed successfully.')); + } else { + $this->flash->failure(t('Unable to remove this task.')); + } + + $redirect = $this->request->getStringParam('redirect') === ''; + $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $task['project_id'])), $redirect); + } +} diff --git a/sources/app/Controller/TaskViewController.php b/sources/app/Controller/TaskViewController.php new file mode 100644 index 0000000..f40f8be --- /dev/null +++ b/sources/app/Controller/TaskViewController.php @@ -0,0 +1,147 @@ +projectModel->getByToken($this->request->getStringParam('token')); + + // Token verification + if (empty($project)) { + throw AccessForbiddenException::getInstance()->withoutLayout(); + } + + $task = $this->taskFinderModel->getDetails($this->request->getIntegerParam('task_id')); + + if (empty($task)) { + throw PageNotFoundException::getInstance()->withoutLayout(); + } + + if ($task['project_id'] != $project['id']) { + throw AccessForbiddenException::getInstance()->withoutLayout(); + } + + $this->response->html($this->helper->layout->app('task/public', array( + 'project' => $project, + 'comments' => $this->commentModel->getAll($task['id']), + 'subtasks' => $this->subtaskModel->getAll($task['id']), + 'links' => $this->taskLinkModel->getAllGroupedByLabel($task['id']), + 'task' => $task, + 'columns_list' => $this->columnModel->getList($task['project_id']), + 'colors_list' => $this->colorModel->getList(), + 'tags' => $this->taskTagModel->getList($task['id']), + 'title' => $task['title'], + 'no_layout' => true, + 'auto_refresh' => true, + 'not_editable' => true, + ))); + } + + /** + * Show a task + * + * @access public + */ + public function show() + { + $task = $this->getTask(); + $subtasks = $this->subtaskModel->getAll($task['id']); + + $values = array( + 'id' => $task['id'], + 'date_started' => $task['date_started'], + 'time_estimated' => $task['time_estimated'] ?: '', + 'time_spent' => $task['time_spent'] ?: '', + ); + + $values = $this->dateParser->format($values, array('date_started'), $this->dateParser->getUserDateTimeFormat()); + + $this->response->html($this->helper->layout->task('task/show', array( + 'task' => $task, + 'project' => $this->projectModel->getById($task['project_id']), + 'values' => $values, + 'files' => $this->taskFileModel->getAllDocuments($task['id']), + 'images' => $this->taskFileModel->getAllImages($task['id']), + 'comments' => $this->commentModel->getAll($task['id'], $this->userSession->getCommentSorting()), + 'subtasks' => $subtasks, + 'internal_links' => $this->taskLinkModel->getAllGroupedByLabel($task['id']), + 'external_links' => $this->taskExternalLinkModel->getAll($task['id']), + 'link_label_list' => $this->linkModel->getList(0, false), + 'tags' => $this->taskTagModel->getList($task['id']), + ))); + } + + /** + * Display task analytics + * + * @access public + */ + public function analytics() + { + $task = $this->getTask(); + + $this->response->html($this->helper->layout->task('task/analytics', array( + 'task' => $task, + 'project' => $this->projectModel->getById($task['project_id']), + 'lead_time' => $this->taskAnalyticModel->getLeadTime($task), + 'cycle_time' => $this->taskAnalyticModel->getCycleTime($task), + 'time_spent_columns' => $this->taskAnalyticModel->getTimeSpentByColumn($task), + ))); + } + + /** + * Display the time tracking details + * + * @access public + */ + public function timetracking() + { + $task = $this->getTask(); + + $subtask_paginator = $this->paginator + ->setUrl('TaskViewController', 'timetracking', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks')) + ->setMax(15) + ->setOrder('start') + ->setDirection('DESC') + ->setQuery($this->subtaskTimeTrackingModel->getTaskQuery($task['id'])) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + + $this->response->html($this->helper->layout->task('task/time_tracking_details', array( + 'task' => $task, + 'project' => $this->projectModel->getById($task['project_id']), + 'subtask_paginator' => $subtask_paginator, + ))); + } + + /** + * Display the task transitions + * + * @access public + */ + public function transitions() + { + $task = $this->getTask(); + + $this->response->html($this->helper->layout->task('task/transitions', array( + 'task' => $task, + 'project' => $this->projectModel->getById($task['project_id']), + 'transitions' => $this->transitionModel->getAllByTask($task['id']), + ))); + } +} diff --git a/sources/app/Controller/Taskmodification.php b/sources/app/Controller/Taskmodification.php deleted file mode 100644 index 6b945f3..0000000 --- a/sources/app/Controller/Taskmodification.php +++ /dev/null @@ -1,122 +0,0 @@ -getTask(); - $this->taskModification->update(array('id' => $task['id'], 'date_started' => time())); - $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); - } - - /** - * Edit description form - * - * @access public - */ - public function description(array $values = array(), array $errors = array()) - { - $task = $this->getTask(); - - if (empty($values)) { - $values = array('id' => $task['id'], 'description' => $task['description']); - } - - $this->response->html($this->template->render('task_modification/edit_description', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - ))); - } - - /** - * Update description - * - * @access public - */ - public function updateDescription() - { - $task = $this->getTask(); - $values = $this->request->getValues(); - - list($valid, $errors) = $this->taskValidator->validateDescriptionCreation($values); - - if ($valid) { - if ($this->taskModification->update($values)) { - $this->flash->success(t('Task updated successfully.')); - } else { - $this->flash->failure(t('Unable to update your task.')); - } - - return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); - } - - $this->description($values, $errors); - } - - /** - * Display a form to edit a task - * - * @access public - */ - public function edit(array $values = array(), array $errors = array()) - { - $task = $this->getTask(); - $project = $this->project->getById($task['project_id']); - - if (empty($values)) { - $values = $task; - $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values)); - $values = $this->hook->merge('controller:task-modification:form:default', $values, array('default_values' => $values)); - } - - $values = $this->dateParser->format($values, array('date_due'), $this->config->get('application_date_format', DateParser::DATE_FORMAT)); - $values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT)); - - $this->response->html($this->template->render('task_modification/edit_task', array( - 'project' => $project, - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id']), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($task['project_id']), - ))); - } - - /** - * Validate and update a task - * - * @access public - */ - public function update() - { - $task = $this->getTask(); - $values = $this->request->getValues(); - - list($valid, $errors) = $this->taskValidator->validateModification($values); - - if ($valid && $this->taskModification->update($values)) { - $this->flash->success(t('Task updated successfully.')); - return $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true); - } else { - $this->flash->failure(t('Unable to update your task.')); - $this->edit($values, $errors); - } - } -} diff --git a/sources/app/Controller/Twofactor.php b/sources/app/Controller/TwoFactorController.php similarity index 81% rename from sources/app/Controller/Twofactor.php rename to sources/app/Controller/TwoFactorController.php index 1029226..d02c895 100644 --- a/sources/app/Controller/Twofactor.php +++ b/sources/app/Controller/TwoFactorController.php @@ -2,23 +2,27 @@ namespace Kanboard\Controller; +use Kanboard\Core\Controller\AccessForbiddenException; + /** * Two Factor Auth controller * - * @package controller + * @package Kanboard/Controller * @author Frederic Guillot */ -class Twofactor extends User +class TwoFactorController extends UserViewController { /** * Only the current user can access to 2FA settings * * @access private + * @param array $user + * @throws AccessForbiddenException */ private function checkCurrentUser(array $user) { if ($user['id'] != $this->userSession->getId()) { - $this->forbidden(); + throw new AccessForbiddenException(); } } @@ -87,7 +91,7 @@ class Twofactor extends User if ($provider->authenticate()) { $this->flash->success(t('The two factor authentication code is valid.')); - $this->user->update(array( + $this->userModel->update(array( 'id' => $user['id'], 'twofactor_activated' => 1, 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(), @@ -96,10 +100,10 @@ class Twofactor extends User unset($this->sessionStorage->twoFactorSecret); $this->userSession->disablePostAuthentication(); - $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + $this->response->redirect($this->helper->url->to('TwoFactorController', 'index', array('user_id' => $user['id']))); } else { $this->flash->failure(t('The two factor authentication code is not valid.')); - $this->response->redirect($this->helper->url->to('twofactor', 'show', array('user_id' => $user['id']))); + $this->response->redirect($this->helper->url->to('TwoFactorController', 'show', array('user_id' => $user['id']))); } } @@ -113,7 +117,7 @@ class Twofactor extends User $user = $this->getUser(); $this->checkCurrentUser($user); - $this->user->update(array( + $this->userModel->update(array( 'id' => $user['id'], 'twofactor_activated' => 0, 'twofactor_secret' => '', @@ -123,7 +127,7 @@ class Twofactor extends User $this->userSession->disablePostAuthentication(); $this->flash->success(t('User updated successfully.')); - $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + $this->response->redirect($this->helper->url->to('TwoFactorController', 'index', array('user_id' => $user['id']))); } /** @@ -145,10 +149,10 @@ class Twofactor extends User if ($provider->authenticate()) { $this->userSession->validatePostAuthentication(); $this->flash->success(t('The two factor authentication code is valid.')); - $this->response->redirect($this->helper->url->to('app', 'index')); + $this->response->redirect($this->helper->url->to('DashboardController', 'show')); } else { $this->flash->failure(t('The two factor authentication code is not valid.')); - $this->response->redirect($this->helper->url->to('twofactor', 'code')); + $this->response->redirect($this->helper->url->to('TwoFactorController', 'code')); } } @@ -182,16 +186,16 @@ class Twofactor extends User if ($this->request->getStringParam('disable') === 'yes') { $this->checkCSRFParam(); - $this->user->update(array( + $this->userModel->update(array( 'id' => $user['id'], 'twofactor_activated' => 0, 'twofactor_secret' => '', )); - $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id']))); + return $this->response->redirect($this->helper->url->to('UserViewController', 'show', array('user_id' => $user['id']))); } - $this->response->html($this->helper->layout->user('twofactor/disable', array( + return $this->response->html($this->helper->layout->user('twofactor/disable', array( 'user' => $user, ))); } diff --git a/sources/app/Controller/User.php b/sources/app/Controller/User.php deleted file mode 100644 index f7d7d2e..0000000 --- a/sources/app/Controller/User.php +++ /dev/null @@ -1,408 +0,0 @@ -paginator - ->setUrl('user', 'index') - ->setMax(30) - ->setOrder('username') - ->setQuery($this->user->getQuery()) - ->calculate(); - - $this->response->html( - $this->helper->layout->app('user/index', array( - 'title' => t('Users').' ('.$paginator->getTotal().')', - 'paginator' => $paginator, - ) - )); - } - - /** - * Public user profile - * - * @access public - */ - public function profile() - { - $user = $this->user->getById($this->request->getIntegerParam('user_id')); - - if (empty($user)) { - $this->notfound(); - } - - $this->response->html( - $this->helper->layout->app('user/profile', array( - 'title' => $user['name'] ?: $user['username'], - 'user' => $user, - ) - )); - } - - /** - * Display a form to create a new user - * - * @access public - */ - public function create(array $values = array(), array $errors = array()) - { - $is_remote = $this->request->getIntegerParam('remote') == 1 || (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1); - - $this->response->html($this->helper->layout->app($is_remote ? 'user/create_remote' : 'user/create_local', array( - 'timezones' => $this->config->getTimezones(true), - 'languages' => $this->config->getLanguages(true), - 'roles' => $this->role->getApplicationRoles(), - 'projects' => $this->project->getList(), - 'errors' => $errors, - 'values' => $values + array('role' => Role::APP_USER), - 'title' => t('New user') - ))); - } - - /** - * Validate and save a new user - * - * @access public - */ - public function save() - { - $values = $this->request->getValues(); - list($valid, $errors) = $this->userValidator->validateCreation($values); - - if ($valid) { - $project_id = empty($values['project_id']) ? 0 : $values['project_id']; - unset($values['project_id']); - - $user_id = $this->user->create($values); - - if ($user_id !== false) { - $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MEMBER); - - if (! empty($values['notifications_enabled'])) { - $this->userNotificationType->saveSelectedTypes($user_id, array(MailNotification::TYPE)); - } - - $this->flash->success(t('User created successfully.')); - $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user_id))); - } else { - $this->flash->failure(t('Unable to create your user.')); - $values['project_id'] = $project_id; - } - } - - $this->create($values, $errors); - } - - /** - * Display user information - * - * @access public - */ - public function show() - { - $user = $this->getUser(); - $this->response->html($this->helper->layout->user('user/show', array( - 'user' => $user, - 'timezones' => $this->config->getTimezones(true), - 'languages' => $this->config->getLanguages(true), - ))); - } - - /** - * 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->helper->layout->user('user/timesheet', array( - 'subtask_paginator' => $subtask_paginator, - 'user' => $user, - ))); - } - - /** - * Display last password reset - * - * @access public - */ - public function passwordReset() - { - $user = $this->getUser(); - $this->response->html($this->helper->layout->user('user/password_reset', array( - 'tokens' => $this->passwordReset->getAll($user['id']), - 'user' => $user, - ))); - } - - /** - * Display last connections - * - * @access public - */ - public function last() - { - $user = $this->getUser(); - $this->response->html($this->helper->layout->user('user/last', array( - 'last_logins' => $this->lastLogin->getAll($user['id']), - 'user' => $user, - ))); - } - - /** - * Display user sessions - * - * @access public - */ - public function sessions() - { - $user = $this->getUser(); - $this->response->html($this->helper->layout->user('user/sessions', array( - 'sessions' => $this->rememberMeSession->getAll($user['id']), - 'user' => $user, - ))); - } - - /** - * Remove a "RememberMe" token - * - * @access public - */ - public function removeSession() - { - $this->checkCSRFParam(); - $user = $this->getUser(); - $this->rememberMeSession->remove($this->request->getIntegerParam('id')); - $this->response->redirect($this->helper->url->to('user', 'sessions', array('user_id' => $user['id']))); - } - - /** - * Display user notifications - * - * @access public - */ - public function notifications() - { - $user = $this->getUser(); - - if ($this->request->isPost()) { - $values = $this->request->getValues(); - $this->userNotification->saveSettings($user['id'], $values); - $this->flash->success(t('User updated successfully.')); - $this->response->redirect($this->helper->url->to('user', 'notifications', array('user_id' => $user['id']))); - } - - $this->response->html($this->helper->layout->user('user/notifications', array( - 'projects' => $this->projectUserRole->getProjectsByUser($user['id'], array(ProjectModel::ACTIVE)), - 'notifications' => $this->userNotification->readSettings($user['id']), - 'types' => $this->userNotificationType->getTypes(), - 'filters' => $this->userNotificationFilter->getFilters(), - 'user' => $user, - ))); - } - - /** - * Display user integrations - * - * @access public - */ - public function integrations() - { - $user = $this->getUser(); - - if ($this->request->isPost()) { - $values = $this->request->getValues(); - $this->userMetadata->save($user['id'], $values); - $this->flash->success(t('User updated successfully.')); - $this->response->redirect($this->helper->url->to('user', 'integrations', array('user_id' => $user['id']))); - } - - $this->response->html($this->helper->layout->user('user/integrations', array( - 'user' => $user, - 'values' => $this->userMetadata->getall($user['id']), - ))); - } - - /** - * Display external accounts - * - * @access public - */ - public function external() - { - $user = $this->getUser(); - $this->response->html($this->helper->layout->user('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->flash->success(t('User updated successfully.')); - } else { - $this->flash->failure(t('Unable to update this user.')); - } - - $this->response->redirect($this->helper->url->to('user', 'share', array('user_id' => $user['id']))); - } - - $this->response->html($this->helper->layout->user('user/share', array( - 'user' => $user, - 'title' => t('Public access'), - ))); - } - - /** - * Password modification - * - * @access public - */ - public function password() - { - $user = $this->getUser(); - $values = array('id' => $user['id']); - $errors = array(); - - if ($this->request->isPost()) { - $values = $this->request->getValues(); - list($valid, $errors) = $this->userValidator->validatePasswordModification($values); - - if ($valid) { - if ($this->user->update($values)) { - $this->flash->success(t('Password modified successfully.')); - } else { - $this->flash->failure(t('Unable to change the password.')); - } - - $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id']))); - } - } - - $this->response->html($this->helper->layout->user('user/password', array( - 'values' => $values, - 'errors' => $errors, - 'user' => $user, - ))); - } - - /** - * Display a form to edit a user - * - * @access public - */ - public function edit() - { - $user = $this->getUser(); - $values = $user; - $errors = array(); - - unset($values['password']); - - if ($this->request->isPost()) { - $values = $this->request->getValues(); - - if (! $this->userSession->isAdmin()) { - if (isset($values['role'])) { - unset($values['role']); - } - } - - list($valid, $errors) = $this->userValidator->validateModification($values); - - if ($valid) { - if ($this->user->update($values)) { - $this->flash->success(t('User updated successfully.')); - } else { - $this->flash->failure(t('Unable to update your user.')); - } - - $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id']))); - } - } - - $this->response->html($this->helper->layout->user('user/edit', array( - 'values' => $values, - 'errors' => $errors, - 'user' => $user, - 'timezones' => $this->config->getTimezones(true), - 'languages' => $this->config->getLanguages(true), - 'roles' => $this->role->getApplicationRoles(), - ))); - } - - /** - * Display a form to edit authentication - * - * @access public - */ - public function authentication() - { - $user = $this->getUser(); - $values = $user; - $errors = array(); - - unset($values['password']); - - if ($this->request->isPost()) { - $values = $this->request->getValues() + array('disable_login_form' => 0, 'is_ldap_user' => 0); - list($valid, $errors) = $this->userValidator->validateModification($values); - - if ($valid) { - if ($this->user->update($values)) { - $this->flash->success(t('User updated successfully.')); - } else { - $this->flash->failure(t('Unable to update your user.')); - } - - $this->response->redirect($this->helper->url->to('user', 'authentication', array('user_id' => $user['id']))); - } - } - - $this->response->html($this->helper->layout->user('user/authentication', array( - 'values' => $values, - 'errors' => $errors, - 'user' => $user, - ))); - } -} diff --git a/sources/app/Controller/UserAjaxController.php b/sources/app/Controller/UserAjaxController.php new file mode 100644 index 0000000..ed18047 --- /dev/null +++ b/sources/app/Controller/UserAjaxController.php @@ -0,0 +1,52 @@ +request->getStringParam('term'); + $filter = $this->userQuery->withFilter(new UserNameFilter($search)); + $filter->getQuery()->asc(UserModel::TABLE.'.name')->asc(UserModel::TABLE.'.username'); + $this->response->json($filter->format(new UserAutoCompleteFormatter($this->container))); + } + + /** + * User mention auto-completion (Ajax) + * + * @access public + */ + public function mention() + { + $project_id = $this->request->getStringParam('project_id'); + $query = $this->request->getStringParam('q'); + $users = $this->projectPermissionModel->findUsernames($project_id, $query); + $this->response->json($users); + } + + /** + * Check if the user is connected + * + * @access public + */ + public function status() + { + $this->response->text('OK'); + } +} diff --git a/sources/app/Controller/UserCreationController.php b/sources/app/Controller/UserCreationController.php new file mode 100644 index 0000000..9c873f8 --- /dev/null +++ b/sources/app/Controller/UserCreationController.php @@ -0,0 +1,83 @@ +request->getIntegerParam('remote') == 1 || (isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1); + $template = $isRemote ? 'user_creation/remote' : 'user_creation/local'; + + $this->response->html($this->template->render($template, array( + 'timezones' => $this->timezoneModel->getTimezones(true), + 'languages' => $this->languageModel->getLanguages(true), + 'roles' => $this->role->getApplicationRoles(), + 'projects' => $this->projectModel->getList(), + 'errors' => $errors, + 'values' => $values + array('role' => Role::APP_USER), + ))); + } + + /** + * Validate and save a new user + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->userValidator->validateCreation($values); + + if ($valid) { + $this->createUser($values); + } else { + $this->show($values, $errors); + } + } + + /** + * Create user + * + * @param array $values + */ + private function createUser(array $values) + { + $project_id = empty($values['project_id']) ? 0 : $values['project_id']; + unset($values['project_id']); + + $user_id = $this->userModel->create($values); + + if ($user_id !== false) { + if ($project_id !== 0) { + $this->projectUserRoleModel->addUser($project_id, $user_id, Role::PROJECT_MEMBER); + } + + if (! empty($values['notifications_enabled'])) { + $this->userNotificationTypeModel->saveSelectedTypes($user_id, array(MailNotification::TYPE)); + } + + $this->flash->success(t('User created successfully.')); + $this->response->redirect($this->helper->url->to('UserViewController', 'show', array('user_id' => $user_id))); + } else { + $this->flash->failure(t('Unable to create your user.')); + $this->response->redirect($this->helper->url->to('UserListController', 'show')); + } + } +} diff --git a/sources/app/Controller/UserCredentialController.php b/sources/app/Controller/UserCredentialController.php new file mode 100644 index 0000000..4021dc3 --- /dev/null +++ b/sources/app/Controller/UserCredentialController.php @@ -0,0 +1,109 @@ +getUser(); + + return $this->response->html($this->helper->layout->user('user_credential/password', array( + 'values' => $values + array('id' => $user['id']), + 'errors' => $errors, + 'user' => $user, + ))); + } + + /** + * Save new password + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + * @throws \Kanboard\Core\Controller\PageNotFoundException + */ + public function savePassword() + { + $user = $this->getUser(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->userValidator->validatePasswordModification($values); + + if ($valid) { + if ($this->userModel->update($values)) { + $this->flash->success(t('Password modified successfully.')); + $this->userLockingModel->resetFailedLogin($user['username']); + } else { + $this->flash->failure(t('Unable to change the password.')); + } + + return $this->response->redirect($this->helper->url->to('UserViewController', 'show', array('user_id' => $user['id']))); + } + + return $this->changePassword($values, $errors); + } + + /** + * Display a form to edit authentication + * + * @access public + * @param array $values + * @param array $errors + * @throws \Kanboard\Core\Controller\AccessForbiddenException + * @throws \Kanboard\Core\Controller\PageNotFoundException + */ + public function changeAuthentication(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + if (empty($values)) { + $values = $user; + unset($values['password']); + } + + return $this->response->html($this->helper->layout->user('user_credential/authentication', array( + 'values' => $values, + 'errors' => $errors, + 'user' => $user, + ))); + } + + /** + * Save authentication + * + * @throws \Kanboard\Core\Controller\AccessForbiddenException + * @throws \Kanboard\Core\Controller\PageNotFoundException + */ + public function saveAuthentication() + { + $user = $this->getUser(); + $values = $this->request->getValues() + array('disable_login_form' => 0, 'is_ldap_user' => 0); + list($valid, $errors) = $this->userValidator->validateModification($values); + + if ($valid) { + if ($this->userModel->update($values)) { + $this->flash->success(t('User updated successfully.')); + } else { + $this->flash->failure(t('Unable to update your user.')); + } + + return $this->response->redirect($this->helper->url->to('UserCredentialController', 'changeAuthentication', array('user_id' => $user['id']))); + } + + return $this->changeAuthentication($values, $errors); + } +} diff --git a/sources/app/Controller/UserHelper.php b/sources/app/Controller/UserHelper.php deleted file mode 100644 index 041ed2c..0000000 --- a/sources/app/Controller/UserHelper.php +++ /dev/null @@ -1,37 +0,0 @@ -request->getStringParam('term'); - $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format(); - $this->response->json($users); - } - - /** - * User mention autocompletion (Ajax) - * - * @access public - */ - public function mention() - { - $project_id = $this->request->getStringParam('project_id'); - $query = $this->request->getStringParam('q'); - $users = $this->projectPermission->findUsernames($project_id, $query); - $this->response->json($users); - } -} diff --git a/sources/app/Controller/UserImport.php b/sources/app/Controller/UserImportController.php similarity index 61% rename from sources/app/Controller/UserImport.php rename to sources/app/Controller/UserImportController.php index debd69e..fec9a31 100644 --- a/sources/app/Controller/UserImport.php +++ b/sources/app/Controller/UserImportController.php @@ -7,40 +7,63 @@ use Kanboard\Core\Csv; /** * User Import controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class UserImport extends Base +class UserImportController extends BaseController { /** * Upload the file and ask settings * + * @param array $values + * @param array $errors */ - public function step1(array $values = array(), array $errors = array()) + public function show(array $values = array(), array $errors = array()) { - $this->response->html($this->helper->layout->app('user_import/step1', array( + $this->response->html($this->template->render('user_import/show', array( 'values' => $values, 'errors' => $errors, 'max_size' => ini_get('upload_max_filesize'), 'delimiters' => Csv::getDelimiters(), 'enclosures' => Csv::getEnclosures(), - 'title' => t('Import users from CSV file'), ))); } /** - * Process CSV file - * + * Submit form */ - public function step2() + public function save() { $values = $this->request->getValues(); $filename = $this->request->getFilePath('file'); if (! file_exists($filename)) { - $this->step1($values, array('file' => array(t('Unable to read your file')))); + $this->flash->failure(t('Unable to read your file')); + } else { + $this->importFile($values, $filename); } + $this->response->redirect($this->helper->url->to('UserListController', 'show')); + } + + /** + * Generate template + * + */ + public function template() + { + $this->response->withFileDownload('users.csv'); + $this->response->csv(array($this->userImport->getColumnMapping())); + } + + /** + * Process file + * + * @param array $values + * @param $filename + */ + private function importFile(array $values, $filename) + { $csv = new Csv($values['delimiter'], $values['enclosure']); $csv->setColumnMapping($this->userImport->getColumnMapping()); $csv->read($filename, array($this->userImport, 'import')); @@ -50,17 +73,5 @@ class UserImport extends Base } else { $this->flash->failure(t('Nothing have been imported!')); } - - $this->response->redirect($this->helper->url->to('userImport', 'step1')); - } - - /** - * Generate template - * - */ - public function template() - { - $this->response->forceDownload('users.csv'); - $this->response->csv(array($this->userImport->getColumnMapping())); } } diff --git a/sources/app/Controller/UserListController.php b/sources/app/Controller/UserListController.php new file mode 100644 index 0000000..31fcdd4 --- /dev/null +++ b/sources/app/Controller/UserListController.php @@ -0,0 +1,32 @@ +paginator + ->setUrl('UserListController', 'show') + ->setMax(30) + ->setOrder('username') + ->setQuery($this->userModel->getQuery()) + ->calculate(); + + $this->response->html($this->helper->layout->app('user_list/show', array( + 'title' => t('Users').' ('.$paginator->getTotal().')', + 'paginator' => $paginator, + ))); + } +} diff --git a/sources/app/Controller/UserModificationController.php b/sources/app/Controller/UserModificationController.php new file mode 100644 index 0000000..d339fd9 --- /dev/null +++ b/sources/app/Controller/UserModificationController.php @@ -0,0 +1,69 @@ +getUser(); + + if (empty($values)) { + $values = $user; + unset($values['password']); + } + + return $this->response->html($this->helper->layout->user('user_modification/show', array( + 'values' => $values, + 'errors' => $errors, + 'user' => $user, + 'timezones' => $this->timezoneModel->getTimezones(true), + 'languages' => $this->languageModel->getLanguages(true), + 'roles' => $this->role->getApplicationRoles(), + ))); + } + + /** + * Save user information + */ + public function save() + { + $user = $this->getUser(); + $values = $this->request->getValues(); + + if (! $this->userSession->isAdmin()) { + if (isset($values['role'])) { + unset($values['role']); + } + } + + list($valid, $errors) = $this->userValidator->validateModification($values); + + if ($valid) { + if ($this->userModel->update($values)) { + $this->flash->success(t('User updated successfully.')); + } else { + $this->flash->failure(t('Unable to update your user.')); + } + + return $this->response->redirect($this->helper->url->to('UserViewController', 'show', array('user_id' => $user['id']))); + } + + return $this->show($values, $errors); + } +} diff --git a/sources/app/Controller/UserStatus.php b/sources/app/Controller/UserStatusController.php similarity index 79% rename from sources/app/Controller/UserStatus.php rename to sources/app/Controller/UserStatusController.php index b8ee5c9..070fb6f 100644 --- a/sources/app/Controller/UserStatus.php +++ b/sources/app/Controller/UserStatusController.php @@ -5,10 +5,10 @@ namespace Kanboard\Controller; /** * User Status Controller * - * @package controller + * @package Kanboard\Controller * @author Frederic Guillot */ -class UserStatus extends Base +class UserStatusController extends BaseController { /** * Confirm remove a user @@ -34,13 +34,13 @@ class UserStatus extends Base $user = $this->getUser(); $this->checkCSRFParam(); - if ($this->user->remove($user['id'])) { + if ($this->userModel->remove($user['id'])) { $this->flash->success(t('User removed successfully.')); } else { $this->flash->failure(t('Unable to remove this user.')); } - $this->response->redirect($this->helper->url->to('user', 'index')); + $this->response->redirect($this->helper->url->to('UserListController', 'show')); } /** @@ -67,13 +67,13 @@ class UserStatus extends Base $user = $this->getUser(); $this->checkCSRFParam(); - if ($this->user->enable($user['id'])) { + if ($this->userModel->enable($user['id'])) { $this->flash->success(t('User activated successfully.')); } else { $this->flash->failure(t('Unable to enable this user.')); } - $this->response->redirect($this->helper->url->to('user', 'index')); + $this->response->redirect($this->helper->url->to('UserListController', 'show')); } /** @@ -100,12 +100,12 @@ class UserStatus extends Base $user = $this->getUser(); $this->checkCSRFParam(); - if ($this->user->disable($user['id'])) { + if ($this->userModel->disable($user['id'])) { $this->flash->success(t('User disabled successfully.')); } else { $this->flash->failure(t('Unable to disable this user.')); } - $this->response->redirect($this->helper->url->to('user', 'index')); + $this->response->redirect($this->helper->url->to('UserListController', 'show')); } } diff --git a/sources/app/Controller/UserViewController.php b/sources/app/Controller/UserViewController.php new file mode 100644 index 0000000..a73c5c5 --- /dev/null +++ b/sources/app/Controller/UserViewController.php @@ -0,0 +1,217 @@ +userModel->getById($this->request->getIntegerParam('user_id')); + + if (empty($user)) { + throw new PageNotFoundException(); + } + + $this->response->html($this->helper->layout->app('user_view/profile', array( + 'title' => $user['name'] ?: $user['username'], + 'user' => $user, + ))); + } + + /** + * Display user information + * + * @access public + */ + public function show() + { + $user = $this->getUser(); + $this->response->html($this->helper->layout->user('user_view/show', array( + 'user' => $user, + 'timezones' => $this->timezoneModel->getTimezones(true), + 'languages' => $this->languageModel->getLanguages(true), + ))); + } + + /** + * Display timesheet + * + * @access public + */ + public function timesheet() + { + $user = $this->getUser(); + + $subtask_paginator = $this->paginator + ->setUrl('UserViewController', 'timesheet', array('user_id' => $user['id'], 'pagination' => 'subtasks')) + ->setMax(20) + ->setOrder('start') + ->setDirection('DESC') + ->setQuery($this->subtaskTimeTrackingModel->getUserQuery($user['id'])) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + + $this->response->html($this->helper->layout->user('user_view/timesheet', array( + 'subtask_paginator' => $subtask_paginator, + 'user' => $user, + ))); + } + + /** + * Display last password reset + * + * @access public + */ + public function passwordReset() + { + $user = $this->getUser(); + $this->response->html($this->helper->layout->user('user_view/password_reset', array( + 'tokens' => $this->passwordResetModel->getAll($user['id']), + 'user' => $user, + ))); + } + + /** + * Display last connections + * + * @access public + */ + public function lastLogin() + { + $user = $this->getUser(); + $this->response->html($this->helper->layout->user('user_view/last', array( + 'last_logins' => $this->lastLoginModel->getAll($user['id']), + 'user' => $user, + ))); + } + + /** + * Display user sessions + * + * @access public + */ + public function sessions() + { + $user = $this->getUser(); + $this->response->html($this->helper->layout->user('user_view/sessions', array( + 'sessions' => $this->rememberMeSessionModel->getAll($user['id']), + 'user' => $user, + ))); + } + + /** + * Remove a "RememberMe" token + * + * @access public + */ + public function removeSession() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + $this->rememberMeSessionModel->remove($this->request->getIntegerParam('id')); + $this->response->redirect($this->helper->url->to('UserViewController', 'sessions', array('user_id' => $user['id']))); + } + + /** + * Display user notifications + * + * @access public + */ + public function notifications() + { + $user = $this->getUser(); + + if ($this->request->isPost()) { + $values = $this->request->getValues(); + $this->userNotificationModel->saveSettings($user['id'], $values); + $this->flash->success(t('User updated successfully.')); + return $this->response->redirect($this->helper->url->to('UserViewController', 'notifications', array('user_id' => $user['id']))); + } + + return $this->response->html($this->helper->layout->user('user_view/notifications', array( + 'projects' => $this->projectUserRoleModel->getProjectsByUser($user['id'], array(ProjectModel::ACTIVE)), + 'notifications' => $this->userNotificationModel->readSettings($user['id']), + 'types' => $this->userNotificationTypeModel->getTypes(), + 'filters' => $this->userNotificationFilterModel->getFilters(), + 'user' => $user, + ))); + } + + /** + * Display user integrations + * + * @access public + */ + public function integrations() + { + $user = $this->getUser(); + + if ($this->request->isPost()) { + $values = $this->request->getValues(); + $this->userMetadataModel->save($user['id'], $values); + $this->flash->success(t('User updated successfully.')); + $this->response->redirect($this->helper->url->to('UserViewController', 'integrations', array('user_id' => $user['id']))); + } + + $this->response->html($this->helper->layout->user('user_view/integrations', array( + 'user' => $user, + 'values' => $this->userMetadataModel->getAll($user['id']), + ))); + } + + /** + * Display external accounts + * + * @access public + */ + public function external() + { + $user = $this->getUser(); + $this->response->html($this->helper->layout->user('user_view/external', array( + 'last_logins' => $this->lastLoginModel->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->userModel->{$switch . 'PublicAccess'}($user['id'])) { + $this->flash->success(t('User updated successfully.')); + } else { + $this->flash->failure(t('Unable to update this user.')); + } + + return $this->response->redirect($this->helper->url->to('UserViewController', 'share', array('user_id' => $user['id']))); + } + + return $this->response->html($this->helper->layout->user('user_view/share', array( + 'user' => $user, + 'title' => t('Public access'), + ))); + } +} diff --git a/sources/app/Controller/WebNotification.php b/sources/app/Controller/WebNotification.php deleted file mode 100644 index dca5cb4..0000000 --- a/sources/app/Controller/WebNotification.php +++ /dev/null @@ -1,50 +0,0 @@ -getUserId(); - - $this->userUnreadNotification->markAllAsRead($user_id); - $this->response->redirect($this->helper->url->to('app', 'notifications', array('user_id' => $user_id))); - } - - /** - * Mark a notification as read - * - * @access public - */ - public function remove() - { - $user_id = $this->getUserId(); - $notification_id = $this->request->getIntegerParam('notification_id'); - - $this->userUnreadNotification->markAsRead($user_id, $notification_id); - $this->response->redirect($this->helper->url->to('app', 'notifications', array('user_id' => $user_id))); - } - - private function getUserId() - { - $user_id = $this->request->getIntegerParam('user_id'); - - if (! $this->userSession->isAdmin() && $user_id != $this->userSession->getId()) { - $user_id = $this->userSession->getId(); - } - - return $user_id; - } -} diff --git a/sources/app/Controller/WebNotificationController.php b/sources/app/Controller/WebNotificationController.php new file mode 100644 index 0000000..30e317f --- /dev/null +++ b/sources/app/Controller/WebNotificationController.php @@ -0,0 +1,79 @@ +getUserId(); + + $this->userUnreadNotificationModel->markAllAsRead($user_id); + $this->response->redirect($this->helper->url->to('DashboardController', 'notifications', array('user_id' => $user_id))); + } + + /** + * Mark a notification as read + * + * @access public + */ + public function remove() + { + $user_id = $this->getUserId(); + $notification_id = $this->request->getIntegerParam('notification_id'); + + $this->userUnreadNotificationModel->markAsRead($user_id, $notification_id); + $this->response->redirect($this->helper->url->to('DashboardController', 'notifications', array('user_id' => $user_id))); + } + + /** + * Redirect to the task and mark notification as read + */ + public function redirect() + { + $user_id = $this->getUserId(); + $notification_id = $this->request->getIntegerParam('notification_id'); + + $notification = $this->userUnreadNotificationModel->getById($notification_id); + $this->userUnreadNotificationModel->markAsRead($user_id, $notification_id); + + if (empty($notification)) { + $this->response->redirect($this->helper->url->to('DashboardController', 'notifications', array('user_id' => $user_id))); + } elseif ($this->helper->text->contains($notification['event_name'], 'comment')) { + $this->response->redirect($this->helper->url->to( + 'TaskViewController', + 'show', + array('task_id' => $this->notificationModel->getTaskIdFromEvent($notification['event_name'], $notification['event_data'])), + 'comment-'.$notification['event_data']['comment']['id'] + )); + } else { + $this->response->redirect($this->helper->url->to( + 'TaskViewController', + 'show', + array('task_id' => $this->notificationModel->getTaskIdFromEvent($notification['event_name'], $notification['event_data'])) + )); + } + } + + private function getUserId() + { + $user_id = $this->request->getIntegerParam('user_id'); + + if (! $this->userSession->isAdmin() && $user_id != $this->userSession->getId()) { + $user_id = $this->userSession->getId(); + } + + return $user_id; + } +} diff --git a/sources/app/Controller/Webhook.php b/sources/app/Controller/Webhook.php deleted file mode 100644 index 0eafe3e..0000000 --- a/sources/app/Controller/Webhook.php +++ /dev/null @@ -1,42 +0,0 @@ -checkWebhookToken(); - - $defaultProject = $this->project->getFirst(); - - $values = array( - 'title' => $this->request->getStringParam('title'), - 'description' => $this->request->getStringParam('description'), - 'color_id' => $this->request->getStringParam('color_id'), - 'project_id' => $this->request->getIntegerParam('project_id', $defaultProject['id']), - 'owner_id' => $this->request->getIntegerParam('owner_id'), - 'column_id' => $this->request->getIntegerParam('column_id'), - 'category_id' => $this->request->getIntegerParam('category_id'), - ); - - list($valid, ) = $this->taskValidator->validateCreation($values); - - if ($valid && $this->taskCreation->create($values)) { - $this->response->text('OK'); - } - - $this->response->text('FAILED'); - } -} diff --git a/sources/app/Core/Action/ActionManager.php b/sources/app/Core/Action/ActionManager.php index f1ea8ab..1dfd820 100644 --- a/sources/app/Core/Action/ActionManager.php +++ b/sources/app/Core/Action/ActionManager.php @@ -18,7 +18,7 @@ class ActionManager extends Base * List of automatic actions * * @access private - * @var array + * @var ActionBase[] */ private $actions = array(); @@ -121,9 +121,9 @@ class ActionManager extends Base public function attachEvents() { if ($this->userSession->isLogged()) { - $actions = $this->action->getAllByUser($this->userSession->getId()); + $actions = $this->actionModel->getAllByUser($this->userSession->getId()); } else { - $actions = $this->action->getAll(); + $actions = $this->actionModel->getAll(); } foreach ($actions as $action) { diff --git a/sources/app/Core/Base.php b/sources/app/Core/Base.php index 74573e9..8103ec1 100644 --- a/sources/app/Core/Base.php +++ b/sources/app/Core/Base.php @@ -10,136 +10,151 @@ use Pimple\Container; * @package core * @author Frederic Guillot * - * @property \Kanboard\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic - * @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic - * @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic - * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic - * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic - * @property \Kanboard\Core\Action\ActionManager $actionManager - * @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager - * @property \Kanboard\Core\Cache\MemoryCache $memoryCache - * @property \Kanboard\Core\Event\EventManager $eventManager - * @property \Kanboard\Core\Group\GroupManager $groupManager - * @property \Kanboard\Core\Http\Client $httpClient - * @property \Kanboard\Core\Http\OAuth2 $oauth - * @property \Kanboard\Core\Http\RememberMeCookie $rememberMeCookie - * @property \Kanboard\Core\Http\Request $request - * @property \Kanboard\Core\Http\Response $response - * @property \Kanboard\Core\Http\Router $router - * @property \Kanboard\Core\Http\Route $route - * @property \Kanboard\Core\Mail\Client $emailClient - * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage - * @property \Kanboard\Core\Plugin\Hook $hook - * @property \Kanboard\Core\Plugin\Loader $pluginLoader - * @property \Kanboard\Core\Security\AuthenticationManager $authenticationManager - * @property \Kanboard\Core\Security\AccessMap $applicationAccessMap - * @property \Kanboard\Core\Security\AccessMap $projectAccessMap - * @property \Kanboard\Core\Security\Authorization $applicationAuthorization - * @property \Kanboard\Core\Security\Authorization $projectAuthorization - * @property \Kanboard\Core\Security\Role $role - * @property \Kanboard\Core\Security\Token $token - * @property \Kanboard\Core\Session\FlashMessage $flash - * @property \Kanboard\Core\Session\SessionManager $sessionManager - * @property \Kanboard\Core\Session\SessionStorage $sessionStorage - * @property \Kanboard\Core\User\Avatar\AvatarManager $avatarManager - * @property \Kanboard\Core\User\GroupSync $groupSync - * @property \Kanboard\Core\User\UserProfile $userProfile - * @property \Kanboard\Core\User\UserSync $userSync - * @property \Kanboard\Core\User\UserSession $userSession - * @property \Kanboard\Core\DateParser $dateParser - * @property \Kanboard\Core\Helper $helper - * @property \Kanboard\Core\Lexer $lexer - * @property \Kanboard\Core\Paginator $paginator - * @property \Kanboard\Core\Template $template - * @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter - * @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter - * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter - * @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter - * @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter - * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter - * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter - * @property \Kanboard\Model\Action $action - * @property \Kanboard\Model\ActionParameter $actionParameter - * @property \Kanboard\Model\AvatarFile $avatarFile - * @property \Kanboard\Model\Board $board - * @property \Kanboard\Model\Category $category - * @property \Kanboard\Model\Color $color - * @property \Kanboard\Model\Column $column - * @property \Kanboard\Model\Comment $comment - * @property \Kanboard\Model\Config $config - * @property \Kanboard\Model\Currency $currency - * @property \Kanboard\Model\CustomFilter $customFilter - * @property \Kanboard\Model\TaskFile $taskFile - * @property \Kanboard\Model\ProjectFile $projectFile - * @property \Kanboard\Model\Group $group - * @property \Kanboard\Model\GroupMember $groupMember - * @property \Kanboard\Model\LastLogin $lastLogin - * @property \Kanboard\Model\Link $link - * @property \Kanboard\Model\Notification $notification - * @property \Kanboard\Model\PasswordReset $passwordReset - * @property \Kanboard\Model\Project $project - * @property \Kanboard\Model\ProjectActivity $projectActivity - * @property \Kanboard\Model\ProjectDuplication $projectDuplication - * @property \Kanboard\Model\ProjectDailyColumnStats $projectDailyColumnStats - * @property \Kanboard\Model\ProjectDailyStats $projectDailyStats - * @property \Kanboard\Model\ProjectMetadata $projectMetadata - * @property \Kanboard\Model\ProjectPermission $projectPermission - * @property \Kanboard\Model\ProjectUserRole $projectUserRole - * @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter - * @property \Kanboard\Model\ProjectGroupRole $projectGroupRole - * @property \Kanboard\Model\ProjectNotification $projectNotification - * @property \Kanboard\Model\ProjectNotificationType $projectNotificationType - * @property \Kanboard\Model\RememberMeSession $rememberMeSession - * @property \Kanboard\Model\Subtask $subtask - * @property \Kanboard\Model\SubtaskTimeTracking $subtaskTimeTracking - * @property \Kanboard\Model\Swimlane $swimlane - * @property \Kanboard\Model\Task $task - * @property \Kanboard\Model\TaskAnalytic $taskAnalytic - * @property \Kanboard\Model\TaskCreation $taskCreation - * @property \Kanboard\Model\TaskDuplication $taskDuplication - * @property \Kanboard\Model\TaskExternalLink $taskExternalLink - * @property \Kanboard\Model\TaskFinder $taskFinder - * @property \Kanboard\Model\TaskFilter $taskFilter - * @property \Kanboard\Model\TaskLink $taskLink - * @property \Kanboard\Model\TaskModification $taskModification - * @property \Kanboard\Model\TaskPermission $taskPermission - * @property \Kanboard\Model\TaskPosition $taskPosition - * @property \Kanboard\Model\TaskStatus $taskStatus - * @property \Kanboard\Model\TaskMetadata $taskMetadata - * @property \Kanboard\Model\Transition $transition - * @property \Kanboard\Model\User $user - * @property \Kanboard\Model\UserLocking $userLocking - * @property \Kanboard\Model\UserMention $userMention - * @property \Kanboard\Model\UserNotification $userNotification - * @property \Kanboard\Model\UserNotificationType $userNotificationType - * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter - * @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification - * @property \Kanboard\Model\UserMetadata $userMetadata - * @property \Kanboard\Validator\ActionValidator $actionValidator - * @property \Kanboard\Validator\AuthValidator $authValidator - * @property \Kanboard\Validator\ColumnValidator $columnValidator - * @property \Kanboard\Validator\CategoryValidator $categoryValidator - * @property \Kanboard\Validator\CommentValidator $commentValidator - * @property \Kanboard\Validator\CurrencyValidator $currencyValidator - * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator - * @property \Kanboard\Validator\GroupValidator $groupValidator - * @property \Kanboard\Validator\LinkValidator $linkValidator - * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator - * @property \Kanboard\Validator\ProjectValidator $projectValidator - * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator - * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator - * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator - * @property \Kanboard\Validator\ExternalLinkValidator $externalLinkValidator - * @property \Kanboard\Validator\TaskValidator $taskValidator - * @property \Kanboard\Validator\UserValidator $userValidator - * @property \Kanboard\Import\TaskImport $taskImport - * @property \Kanboard\Import\UserImport $userImport - * @property \Kanboard\Export\SubtaskExport $subtaskExport - * @property \Kanboard\Export\TaskExport $taskExport - * @property \Kanboard\Export\TransitionExport $transitionExport - * @property \Psr\Log\LoggerInterface $logger - * @property \PicoDb\Database $db - * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher + * @property \Kanboard\Analytic\TaskDistributionAnalytic $taskDistributionAnalytic + * @property \Kanboard\Analytic\UserDistributionAnalytic $userDistributionAnalytic + * @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic + * @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic + * @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic + * @property \Kanboard\Core\Action\ActionManager $actionManager + * @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager + * @property \Kanboard\Core\Cache\MemoryCache $memoryCache + * @property \Kanboard\Core\Event\EventManager $eventManager + * @property \Kanboard\Core\Group\GroupManager $groupManager + * @property \Kanboard\Core\Http\Client $httpClient + * @property \Kanboard\Core\Http\OAuth2 $oauth + * @property \Kanboard\Core\Http\RememberMeCookie $rememberMeCookie + * @property \Kanboard\Core\Http\Request $request + * @property \Kanboard\Core\Http\Response $response + * @property \Kanboard\Core\Http\Router $router + * @property \Kanboard\Core\Http\Route $route + * @property \Kanboard\Core\Queue\QueueManager $queueManager + * @property \Kanboard\Core\Mail\Client $emailClient + * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage + * @property \Kanboard\Core\Plugin\Hook $hook + * @property \Kanboard\Core\Plugin\Loader $pluginLoader + * @property \Kanboard\Core\Security\AuthenticationManager $authenticationManager + * @property \Kanboard\Core\Security\AccessMap $applicationAccessMap + * @property \Kanboard\Core\Security\AccessMap $projectAccessMap + * @property \Kanboard\Core\Security\AccessMap $apiAccessMap + * @property \Kanboard\Core\Security\AccessMap $apiProjectAccessMap + * @property \Kanboard\Core\Security\Authorization $applicationAuthorization + * @property \Kanboard\Core\Security\Authorization $projectAuthorization + * @property \Kanboard\Core\Security\Authorization $apiAuthorization + * @property \Kanboard\Core\Security\Authorization $apiProjectAuthorization + * @property \Kanboard\Core\Security\Role $role + * @property \Kanboard\Core\Security\Token $token + * @property \Kanboard\Core\Session\FlashMessage $flash + * @property \Kanboard\Core\Session\SessionManager $sessionManager + * @property \Kanboard\Core\Session\SessionStorage $sessionStorage + * @property \Kanboard\Core\User\Avatar\AvatarManager $avatarManager + * @property \Kanboard\Core\User\GroupSync $groupSync + * @property \Kanboard\Core\User\UserProfile $userProfile + * @property \Kanboard\Core\User\UserSync $userSync + * @property \Kanboard\Core\User\UserSession $userSession + * @property \Kanboard\Core\DateParser $dateParser + * @property \Kanboard\Core\Helper $helper + * @property \Kanboard\Core\Paginator $paginator + * @property \Kanboard\Core\Template $template + * @property \Kanboard\Model\ActionModel $actionModel + * @property \Kanboard\Model\ActionParameterModel $actionParameterModel + * @property \Kanboard\Model\AvatarFileModel $avatarFileModel + * @property \Kanboard\Model\BoardModel $boardModel + * @property \Kanboard\Model\CategoryModel $categoryModel + * @property \Kanboard\Model\ColorModel $colorModel + * @property \Kanboard\Model\ColumnModel $columnModel + * @property \Kanboard\Model\CommentModel $commentModel + * @property \Kanboard\Model\ConfigModel $configModel + * @property \Kanboard\Model\CurrencyModel $currencyModel + * @property \Kanboard\Model\CustomFilterModel $customFilterModel + * @property \Kanboard\Model\TaskFileModel $taskFileModel + * @property \Kanboard\Model\ProjectFileModel $projectFileModel + * @property \Kanboard\Model\GroupModel $groupModel + * @property \Kanboard\Model\GroupMemberModel $groupMemberModel + * @property \Kanboard\Model\LanguageModel $languageModel + * @property \Kanboard\Model\LastLoginModel $lastLoginModel + * @property \Kanboard\Model\LinkModel $linkModel + * @property \Kanboard\Model\NotificationModel $notificationModel + * @property \Kanboard\Model\PasswordResetModel $passwordResetModel + * @property \Kanboard\Model\ProjectModel $projectModel + * @property \Kanboard\Model\ProjectActivityModel $projectActivityModel + * @property \Kanboard\Model\ProjectDuplicationModel $projectDuplicationModel + * @property \Kanboard\Model\ProjectDailyColumnStatsModel $projectDailyColumnStatsModel + * @property \Kanboard\Model\ProjectDailyStatsModel $projectDailyStatsModel + * @property \Kanboard\Model\ProjectMetadataModel $projectMetadataModel + * @property \Kanboard\Model\ProjectPermissionModel $projectPermissionModel + * @property \Kanboard\Model\ProjectUserRoleModel $projectUserRoleModel + * @property \Kanboard\Model\ProjectGroupRoleModel $projectGroupRoleModel + * @property \Kanboard\Model\ProjectNotificationModel $projectNotificationModel + * @property \Kanboard\Model\ProjectNotificationTypeModel $projectNotificationTypeModel + * @property \Kanboard\Model\ProjectTaskDuplicationModel $projectTaskDuplicationModel + * @property \Kanboard\Model\ProjectTaskPriorityModel $projectTaskPriorityModel + * @property \Kanboard\Model\RememberMeSessionModel $rememberMeSessionModel + * @property \Kanboard\Model\SubtaskModel $subtaskModel + * @property \Kanboard\Model\SubtaskTimeTrackingModel $subtaskTimeTrackingModel + * @property \Kanboard\Model\SwimlaneModel $swimlaneModel + * @property \Kanboard\Model\TagDuplicationModel $tagDuplicationModel + * @property \Kanboard\Model\TagModel $tagModel + * @property \Kanboard\Model\TaskModel $taskModel + * @property \Kanboard\Model\TaskAnalyticModel $taskAnalyticModel + * @property \Kanboard\Model\TaskCreationModel $taskCreationModel + * @property \Kanboard\Model\TaskDuplicationModel $taskDuplicationModel + * @property \Kanboard\Model\TaskProjectDuplicationModel $taskProjectDuplicationModel + * @property \Kanboard\Model\TaskProjectMoveModel $taskProjectMoveModel + * @property \Kanboard\Model\TaskRecurrenceModel $taskRecurrenceModel + * @property \Kanboard\Model\TaskExternalLinkModel $taskExternalLinkModel + * @property \Kanboard\Model\TaskFinderModel $taskFinderModel + * @property \Kanboard\Model\TaskLinkModel $taskLinkModel + * @property \Kanboard\Model\TaskModificationModel $taskModificationModel + * @property \Kanboard\Model\TaskPositionModel $taskPositionModel + * @property \Kanboard\Model\TaskStatusModel $taskStatusModel + * @property \Kanboard\Model\TaskTagModel $taskTagModel + * @property \Kanboard\Model\TaskMetadataModel $taskMetadataModel + * @property \Kanboard\Model\TimezoneModel $timezoneModel + * @property \Kanboard\Model\TransitionModel $transitionModel + * @property \Kanboard\Model\UserModel $userModel + * @property \Kanboard\Model\UserLockingModel $userLockingModel + * @property \Kanboard\Model\UserMentionModel $userMentionModel + * @property \Kanboard\Model\UserNotificationModel $userNotificationModel + * @property \Kanboard\Model\UserNotificationTypeModel $userNotificationTypeModel + * @property \Kanboard\Model\UserNotificationFilterModel $userNotificationFilterModel + * @property \Kanboard\Model\UserUnreadNotificationModel $userUnreadNotificationModel + * @property \Kanboard\Model\UserMetadataModel $userMetadataModel + * @property \Kanboard\Validator\ActionValidator $actionValidator + * @property \Kanboard\Validator\AuthValidator $authValidator + * @property \Kanboard\Validator\ColumnValidator $columnValidator + * @property \Kanboard\Validator\CategoryValidator $categoryValidator + * @property \Kanboard\Validator\CommentValidator $commentValidator + * @property \Kanboard\Validator\CurrencyValidator $currencyValidator + * @property \Kanboard\Validator\CustomFilterValidator $customFilterValidator + * @property \Kanboard\Validator\ExternalLinkValidator $externalLinkValidator + * @property \Kanboard\Validator\GroupValidator $groupValidator + * @property \Kanboard\Validator\LinkValidator $linkValidator + * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator + * @property \Kanboard\Validator\ProjectValidator $projectValidator + * @property \Kanboard\Validator\SubtaskValidator $subtaskValidator + * @property \Kanboard\Validator\SwimlaneValidator $swimlaneValidator + * @property \Kanboard\Validator\TagValidator $tagValidator + * @property \Kanboard\Validator\TaskLinkValidator $taskLinkValidator + * @property \Kanboard\Validator\TaskValidator $taskValidator + * @property \Kanboard\Validator\UserValidator $userValidator + * @property \Kanboard\Import\TaskImport $taskImport + * @property \Kanboard\Import\UserImport $userImport + * @property \Kanboard\Export\SubtaskExport $subtaskExport + * @property \Kanboard\Export\TaskExport $taskExport + * @property \Kanboard\Export\TransitionExport $transitionExport + * @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectActivityQuery + * @property \Kanboard\Core\Filter\QueryBuilder $userQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery + * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery + * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer + * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer + * @property \Psr\Log\LoggerInterface $logger + * @property \PicoDb\Database $db + * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher + * @property \Symfony\Component\Console\Application $cli + * @property \JsonRPC\Server $api */ abstract class Base { @@ -173,4 +188,18 @@ abstract class Base { return $this->container[$name]; } + + /** + * Get object instance + * + * @static + * @access public + * @param Container $container + * @return static + */ + public static function getInstance(Container $container) + { + $self = new static($container); + return $self; + } } diff --git a/sources/app/Core/Controller/AccessForbiddenException.php b/sources/app/Core/Controller/AccessForbiddenException.php new file mode 100644 index 0000000..b5dccb7 --- /dev/null +++ b/sources/app/Core/Controller/AccessForbiddenException.php @@ -0,0 +1,14 @@ +withoutLayout = true; + return $this; + } + + /** + * Return true if no layout + * + * @access public + * @return boolean + */ + public function hasLayout() + { + return $this->withoutLayout; + } +} diff --git a/sources/app/Core/Controller/BaseMiddleware.php b/sources/app/Core/Controller/BaseMiddleware.php new file mode 100644 index 0000000..e94ad95 --- /dev/null +++ b/sources/app/Core/Controller/BaseMiddleware.php @@ -0,0 +1,58 @@ +nextMiddleware = $nextMiddleware; + return $this; + } + + /** + * @return BaseMiddleware + */ + public function getNextMiddleware() + { + return $this->nextMiddleware; + } + + /** + * Move to next middleware + */ + public function next() + { + if ($this->nextMiddleware !== null) { + if (DEBUG) { + $this->logger->debug(__METHOD__.' => ' . get_class($this->nextMiddleware)); + } + + $this->nextMiddleware->execute(); + } + } +} diff --git a/sources/app/Core/Controller/PageNotFoundException.php b/sources/app/Core/Controller/PageNotFoundException.php new file mode 100644 index 0000000..e96a205 --- /dev/null +++ b/sources/app/Core/Controller/PageNotFoundException.php @@ -0,0 +1,14 @@ +executeMiddleware(); + + if (!$this->response->isResponseAlreadySent()) { + $this->executeController(); + } + } catch (PageNotFoundException $e) { + $controllerObject = new AppController($this->container); + $controllerObject->notFound($e->hasLayout()); + } catch (AccessForbiddenException $e) { + $controllerObject = new AppController($this->container); + $controllerObject->accessForbidden($e->hasLayout()); + } + } + + /** + * Execute all middleware + */ + protected function executeMiddleware() + { + if (DEBUG) { + $this->logger->debug(__METHOD__); + } + + $bootstrapMiddleware = new BootstrapMiddleware($this->container); + $authenticationMiddleware = new AuthenticationMiddleware($this->container); + $postAuthenticationMiddleware = new PostAuthenticationMiddleware($this->container); + $appAuthorizationMiddleware = new ApplicationAuthorizationMiddleware($this->container); + $projectAuthorizationMiddleware = new ProjectAuthorizationMiddleware($this->container); + + $bootstrapMiddleware->setNextMiddleware($authenticationMiddleware); + $authenticationMiddleware->setNextMiddleware($postAuthenticationMiddleware); + $postAuthenticationMiddleware->setNextMiddleware($appAuthorizationMiddleware); + $appAuthorizationMiddleware->setNextMiddleware($projectAuthorizationMiddleware); + + $bootstrapMiddleware->execute(); + } + + /** + * Execute the controller + */ + protected function executeController() + { + $className = $this->getControllerClassName(); + + if (DEBUG) { + $this->logger->debug(__METHOD__.' => '.$className.'::'.$this->router->getAction()); + } + + $controllerObject = new $className($this->container); + $controllerObject->{$this->router->getAction()}(); + } + + /** + * Get controller class name + * + * @access protected + * @return string + * @throws RuntimeException + */ + protected function getControllerClassName() + { + if ($this->router->getPlugin() !== '') { + $className = '\Kanboard\Plugin\\'.$this->router->getPlugin().'\Controller\\'.$this->router->getController(); + } else { + $className = '\Kanboard\Controller\\'.$this->router->getController(); + } + + if (! class_exists($className)) { + throw new RuntimeException('Controller not found'); + } + + if (! method_exists($className, $this->router->getAction())) { + throw new RuntimeException('Action not implemented'); + } + + return $className; + } +} diff --git a/sources/app/Core/DateParser.php b/sources/app/Core/DateParser.php index 835eb3e..a7b10a7 100644 --- a/sources/app/Core/DateParser.php +++ b/sources/app/Core/DateParser.php @@ -14,6 +14,40 @@ class DateParser extends Base { const DATE_FORMAT = 'm/d/Y'; const DATE_TIME_FORMAT = 'm/d/Y H:i'; + const TIME_FORMAT = 'H:i'; + + /** + * Get date format from settings + * + * @access public + * @return string + */ + public function getUserDateFormat() + { + return $this->configModel->get('application_date_format', DateParser::DATE_FORMAT); + } + + /** + * Get date time format from settings + * + * @access public + * @return string + */ + public function getUserDateTimeFormat() + { + return $this->configModel->get('application_datetime_format', DateParser::DATE_TIME_FORMAT); + } + + /** + * Get time format from settings + * + * @access public + * @return string + */ + public function getUserTimeFormat() + { + return $this->configModel->get('application_time_format', DateParser::TIME_FORMAT); + } /** * List of time formats @@ -38,19 +72,29 @@ class DateParser extends Base */ public function getDateFormats($iso = false) { - $iso_formats = array( + $formats = array( + $this->getUserDateFormat(), + ); + + $isoFormats = array( 'Y-m-d', 'Y_m_d', ); - $user_formats = array( + $userFormats = array( 'm/d/Y', 'd/m/Y', 'Y/m/d', 'd.m.Y', ); - return $iso ? array_merge($iso_formats, $user_formats) : $user_formats; + if ($iso) { + $formats = array_merge($formats, $isoFormats, $userFormats); + } else { + $formats = array_merge($formats, $userFormats); + } + + return array_unique($formats); } /** @@ -62,7 +106,9 @@ class DateParser extends Base */ public function getDateTimeFormats($iso = false) { - $formats = array(); + $formats = array( + $this->getUserDateTimeFormat(), + ); foreach ($this->getDateFormats($iso) as $date) { foreach ($this->getTimeFormats() as $time) { @@ -70,7 +116,7 @@ class DateParser extends Base } } - return $formats; + return array_unique($formats); } /** @@ -97,12 +143,30 @@ class DateParser extends Base $values = array(); foreach ($formats as $format) { - $values[$format] = date($format); + $values[$format] = date($format).' ('.$format.')'; } return $values; } + /** + * Get formats for date parsing + * + * @access public + * @return array + */ + public function getParserFormats() + { + return array( + $this->getUserDateFormat(), + 'Y-m-d', + 'Y_m_d', + $this->getUserDateTimeFormat(), + 'Y-m-d H:i', + 'Y_m_d H:i', + ); + } + /** * Parse a date and return a unix timestamp, try different date formats * @@ -116,7 +180,7 @@ class DateParser extends Base return (int) $value; } - foreach ($this->getAllDateFormats(true) as $format) { + foreach ($this->getParserFormats() as $format) { $timestamp = $this->getValidDate($value, $format); if ($timestamp !== 0) { diff --git a/sources/app/Core/Event/EventManager.php b/sources/app/Core/Event/EventManager.php index 162d23e..9ae4317 100644 --- a/sources/app/Core/Event/EventManager.php +++ b/sources/app/Core/Event/EventManager.php @@ -2,8 +2,8 @@ namespace Kanboard\Core\Event; -use Kanboard\Model\Task; -use Kanboard\Model\TaskLink; +use Kanboard\Model\TaskModel; +use Kanboard\Model\TaskLinkModel; /** * Event Manager @@ -44,15 +44,15 @@ class EventManager public function getAll() { $events = array( - TaskLink::EVENT_CREATE_UPDATE => t('Task link creation or modification'), - Task::EVENT_MOVE_COLUMN => t('Move a task to another column'), - Task::EVENT_UPDATE => t('Task modification'), - Task::EVENT_CREATE => t('Task creation'), - Task::EVENT_OPEN => t('Reopen a task'), - Task::EVENT_CLOSE => t('Closing a task'), - Task::EVENT_CREATE_UPDATE => t('Task creation or modification'), - Task::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), - Task::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'), + TaskLinkModel::EVENT_CREATE_UPDATE => t('Task link creation or modification'), + TaskModel::EVENT_MOVE_COLUMN => t('Move a task to another column'), + TaskModel::EVENT_UPDATE => t('Task modification'), + TaskModel::EVENT_CREATE => t('Task creation'), + TaskModel::EVENT_OPEN => t('Reopen a task'), + TaskModel::EVENT_CLOSE => t('Closing a task'), + TaskModel::EVENT_CREATE_UPDATE => t('Task creation or modification'), + TaskModel::EVENT_ASSIGNEE_CHANGE => t('Task assignee change'), + TaskModel::EVENT_DAILY_CRONJOB => t('Daily background job for tasks'), ); $events = array_merge($events, $this->events); diff --git a/sources/app/Core/ExternalLink/ExternalLinkManager.php b/sources/app/Core/ExternalLink/ExternalLinkManager.php index 1fa423c..5a03799 100644 --- a/sources/app/Core/ExternalLink/ExternalLinkManager.php +++ b/sources/app/Core/ExternalLink/ExternalLinkManager.php @@ -23,7 +23,7 @@ class ExternalLinkManager extends Base * Registered providers * * @access private - * @var array + * @var ExternalLinkProviderInterface[] */ private $providers = array(); @@ -152,6 +152,30 @@ class ExternalLinkManager extends Base return $this; } + /** + * Set provider type + * + * @access public + * @param string $userInputType + * @return ExternalLinkManager + */ + public function setUserInputType($userInputType) + { + $this->userInputType = $userInputType; + return $this; + } + + /** + * Set external link + * @param string $userInputText + * @return ExternalLinkManager + */ + public function setUserInputText($userInputText) + { + $this->userInputText = $userInputText; + return $this; + } + /** * Find a provider that user input * diff --git a/sources/app/Core/Filter/CriteriaInterface.php b/sources/app/Core/Filter/CriteriaInterface.php new file mode 100644 index 0000000..009c4bd --- /dev/null +++ b/sources/app/Core/Filter/CriteriaInterface.php @@ -0,0 +1,40 @@ + 'T_WHITESPACE', + '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_STRING', + '/^([<=>]{1,2}\w+)/u' => 'T_STRING', + '/^([<=>]{1,2}".+")/' => 'T_STRING', + '/^("(.+)")/' => 'T_STRING', + '/^(\S+)/u' => 'T_STRING', + '/^(#\d+)/' => 'T_STRING', + ); + + /** + * Default token + * + * @access private + * @var string + */ + private $defaultToken = ''; + + /** + * Add token + * + * @access public + * @param string $regex + * @param string $token + * @return $this + */ + public function addToken($regex, $token) + { + $this->tokenMap = array($regex => $token) + $this->tokenMap; + return $this; + } + + /** + * Set default token + * + * @access public + * @param string $token + * @return $this + */ + public function setDefaultToken($token) + { + $this->defaultToken = $token; + return $this; + } + + /** + * Tokenize input string + * + * @access public + * @param string $input + * @return array + */ + public function tokenize($input) + { + $tokens = array(); + $this->offset = 0; + $input_length = mb_strlen($input, 'UTF-8'); + + while ($this->offset < $input_length) { + $result = $this->match(mb_substr($input, $this->offset, $input_length, 'UTF-8')); + + if ($result === false) { + return array(); + } + + $tokens[] = $result; + } + + return $this->map($tokens); + } + + /** + * Find a token that match and move the offset + * + * @access protected + * @param string $string + * @return array|boolean + */ + protected function match($string) + { + foreach ($this->tokenMap as $pattern => $name) { + if (preg_match($pattern, $string, $matches)) { + $this->offset += mb_strlen($matches[1], 'UTF-8'); + + return array( + 'match' => str_replace('"', '', $matches[1]), + 'token' => $name, + ); + } + } + + return false; + } + + /** + * Build map of tokens and matches + * + * @access protected + * @param array $tokens + * @return array + */ + protected function map(array $tokens) + { + $map = array(); + $leftOver = ''; + + while (false !== ($token = current($tokens))) { + if ($token['token'] === 'T_STRING' || $token['token'] === 'T_WHITESPACE') { + $leftOver .= $token['match']; + } else { + $next = next($tokens); + + if ($next !== false && $next['token'] === 'T_STRING') { + $map[$token['token']][] = $next['match']; + } + } + + next($tokens); + } + + $leftOver = trim($leftOver); + + if ($this->defaultToken !== '' && $leftOver !== '') { + $map[$this->defaultToken] = array($leftOver); + } + + return $map; + } +} diff --git a/sources/app/Core/Filter/LexerBuilder.php b/sources/app/Core/Filter/LexerBuilder.php new file mode 100644 index 0000000..7a9a714 --- /dev/null +++ b/sources/app/Core/Filter/LexerBuilder.php @@ -0,0 +1,151 @@ +lexer = new Lexer; + $this->queryBuilder = new QueryBuilder(); + } + + /** + * Add a filter + * + * @access public + * @param FilterInterface $filter + * @param bool $default + * @return LexerBuilder + */ + public function withFilter(FilterInterface $filter, $default = false) + { + $attributes = $filter->getAttributes(); + + foreach ($attributes as $attribute) { + $this->filters[$attribute] = $filter; + $this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute); + + if ($default) { + $this->lexer->setDefaultToken($attribute); + } + } + + return $this; + } + + /** + * Set the query + * + * @access public + * @param Table $query + * @return LexerBuilder + */ + public function withQuery(Table $query) + { + $this->query = $query; + $this->queryBuilder->withQuery($this->query); + return $this; + } + + /** + * Parse the input and build the query + * + * @access public + * @param string $input + * @return QueryBuilder + */ + public function build($input) + { + $tokens = $this->lexer->tokenize($input); + + foreach ($tokens as $token => $values) { + if (isset($this->filters[$token])) { + $this->applyFilters($this->filters[$token], $values); + } + } + + return $this->queryBuilder; + } + + /** + * Apply filters to the query + * + * @access protected + * @param FilterInterface $filter + * @param array $values + */ + protected function applyFilters(FilterInterface $filter, array $values) + { + $len = count($values); + + if ($len > 1) { + $criteria = new OrCriteria(); + $criteria->withQuery($this->query); + + foreach ($values as $value) { + $currentFilter = clone($filter); + $criteria->withFilter($currentFilter->withValue($value)); + } + + $this->queryBuilder->withCriteria($criteria); + } elseif ($len === 1) { + $this->queryBuilder->withFilter($filter->withValue($values[0])); + } + } + + /** + * Clone object with deep copy + */ + public function __clone() + { + $this->lexer = clone $this->lexer; + $this->query = clone $this->query; + $this->queryBuilder = clone $this->queryBuilder; + } +} diff --git a/sources/app/Core/Filter/OrCriteria.php b/sources/app/Core/Filter/OrCriteria.php new file mode 100644 index 0000000..174b845 --- /dev/null +++ b/sources/app/Core/Filter/OrCriteria.php @@ -0,0 +1,68 @@ +query = $query; + return $this; + } + + /** + * Set filter + * + * @access public + * @param FilterInterface $filter + * @return CriteriaInterface + */ + public function withFilter(FilterInterface $filter) + { + $this->filters[] = $filter; + return $this; + } + + /** + * Apply condition + * + * @access public + * @return CriteriaInterface + */ + public function apply() + { + $this->query->beginOr(); + + foreach ($this->filters as $filter) { + $filter->withQuery($this->query)->apply(); + } + + $this->query->closeOr(); + return $this; + } +} diff --git a/sources/app/Core/Filter/QueryBuilder.php b/sources/app/Core/Filter/QueryBuilder.php new file mode 100644 index 0000000..3de82b6 --- /dev/null +++ b/sources/app/Core/Filter/QueryBuilder.php @@ -0,0 +1,103 @@ +query = $query; + return $this; + } + + /** + * Set a filter + * + * @access public + * @param FilterInterface $filter + * @return QueryBuilder + */ + public function withFilter(FilterInterface $filter) + { + $filter->withQuery($this->query)->apply(); + return $this; + } + + /** + * Set a criteria + * + * @access public + * @param CriteriaInterface $criteria + * @return QueryBuilder + */ + public function withCriteria(CriteriaInterface $criteria) + { + $criteria->withQuery($this->query)->apply(); + return $this; + } + + /** + * Set a formatter + * + * @access public + * @param FormatterInterface $formatter + * @return string|array + */ + public function format(FormatterInterface $formatter) + { + return $formatter->withQuery($this->query)->format(); + } + + /** + * Get the query result as array + * + * @access public + * @return array + */ + public function toArray() + { + return $this->query->findAll(); + } + + /** + * Get Query object + * + * @access public + * @return Table + */ + public function getQuery() + { + return $this->query; + } + + /** + * Clone object with deep copy + */ + public function __clone() + { + $this->query = clone $this->query; + } +} diff --git a/sources/app/Core/Helper.php b/sources/app/Core/Helper.php index 3a66fbd..43151be 100644 --- a/sources/app/Core/Helper.php +++ b/sources/app/Core/Helper.php @@ -12,10 +12,12 @@ use Pimple\Container; * * @property \Kanboard\Helper\AppHelper $app * @property \Kanboard\Helper\AssetHelper $asset + * @property \Kanboard\Helper\CalendarHelper $calendar * @property \Kanboard\Helper\DateHelper $dt * @property \Kanboard\Helper\FileHelper $file * @property \Kanboard\Helper\FormHelper $form * @property \Kanboard\Helper\HookHelper $hook + * @property \Kanboard\Helper\ICalHelper $ical * @property \Kanboard\Helper\ModelHelper $model * @property \Kanboard\Helper\SubtaskHelper $subtask * @property \Kanboard\Helper\TaskHelper $task @@ -24,6 +26,8 @@ use Pimple\Container; * @property \Kanboard\Helper\UserHelper $user * @property \Kanboard\Helper\LayoutHelper $layout * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader + * @property \Kanboard\Helper\ProjectActivityHelper $projectActivity + * @property \Kanboard\Helper\MailHelper $mail */ class Helper { @@ -91,7 +95,7 @@ class Helper { $container = $this->container; - $this->helpers[$property] = function() use($className, $container) { + $this->helpers[$property] = function() use ($className, $container) { return new $className($container); }; diff --git a/sources/app/Core/Http/Client.php b/sources/app/Core/Http/Client.php index 12b0a1c..3c4397e 100644 --- a/sources/app/Core/Http/Client.php +++ b/sources/app/Core/Http/Client.php @@ -3,6 +3,7 @@ namespace Kanboard\Core\Http; use Kanboard\Core\Base; +use Kanboard\Job\HttpAsyncJob; /** * HTTP client @@ -79,6 +80,24 @@ class Client extends Base ); } + /** + * Send a POST HTTP request encoded in JSON (Fire and forget) + * + * @access public + * @param string $url + * @param array $data + * @param string[] $headers + */ + public function postJsonAsync($url, array $data, array $headers = array()) + { + $this->queueManager->push(HttpAsyncJob::getInstance($this->container)->withParams( + 'POST', + $url, + json_encode($data), + array_merge(array('Content-type: application/json'), $headers) + )); + } + /** * Send a POST HTTP request encoded in www-form-urlencoded * @@ -98,22 +117,41 @@ class Client extends Base ); } + /** + * Send a POST HTTP request encoded in www-form-urlencoded (fire and forget) + * + * @access public + * @param string $url + * @param array $data + * @param string[] $headers + */ + public function postFormAsync($url, array $data, array $headers = array()) + { + $this->queueManager->push(HttpAsyncJob::getInstance($this->container)->withParams( + 'POST', + $url, + http_build_query($data), + array_merge(array('Content-type: application/x-www-form-urlencoded'), $headers) + )); + } + /** * Make the HTTP request * - * @access private + * @access public * @param string $method * @param string $url * @param string $content * @param string[] $headers * @return string */ - private function doRequest($method, $url, $content, array $headers) + public function doRequest($method, $url, $content, array $headers) { if (empty($url)) { return ''; } + $startTime = microtime(true); $stream = @fopen(trim($url), 'r', false, stream_context_create($this->getContext($method, $content, $headers))); $response = ''; @@ -125,9 +163,11 @@ class Client extends Base if (DEBUG) { $this->logger->debug('HttpClient: url='.$url); + $this->logger->debug('HttpClient: headers='.var_export($headers, true)); $this->logger->debug('HttpClient: payload='.$content); $this->logger->debug('HttpClient: metadata='.var_export(@stream_get_meta_data($stream), true)); $this->logger->debug('HttpClient: response='.$response); + $this->logger->debug('HttpClient: executionTime='.(microtime(true) - $startTime)); } return $response; @@ -162,7 +202,7 @@ class Client extends Base 'timeout' => self::HTTP_TIMEOUT, 'max_redirects' => self::HTTP_MAX_REDIRECTS, 'header' => implode("\r\n", $headers), - 'content' => $content + 'content' => $content, ) ); @@ -171,6 +211,14 @@ class Client extends Base $context['http']['request_fulluri'] = true; } + if (HTTP_VERIFY_SSL_CERTIFICATE === false) { + $context['ssl'] = array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ); + } + return $context; } } diff --git a/sources/app/Core/Http/Response.php b/sources/app/Core/Http/Response.php index 37349ca..0f16fb6 100644 --- a/sources/app/Core/Http/Response.php +++ b/sources/app/Core/Http/Response.php @@ -13,72 +13,218 @@ use Kanboard\Core\Csv; */ class Response extends Base { + private $httpStatusCode = 200; + private $httpHeaders = array(); + private $httpBody = ''; + private $responseSent = false; + + /** + * Return true if the response have been sent to the user agent + * + * @access public + * @return bool + */ + public function isResponseAlreadySent() + { + return $this->responseSent; + } + + /** + * Set HTTP status code + * + * @access public + * @param integer $statusCode + * @return $this + */ + public function withStatusCode($statusCode) + { + $this->httpStatusCode = $statusCode; + return $this; + } + + /** + * Set HTTP header + * + * @access public + * @param string $header + * @param string $value + * @return $this + */ + public function withHeader($header, $value) + { + $this->httpHeaders[$header] = $value; + return $this; + } + + /** + * Set content type header + * + * @access public + * @param string $value + * @return $this + */ + public function withContentType($value) + { + $this->httpHeaders['Content-Type'] = $value; + return $this; + } + + /** + * Set default security headers + * + * @access public + * @return $this + */ + public function withSecurityHeaders() + { + $this->httpHeaders['X-Content-Type-Options'] = 'nosniff'; + $this->httpHeaders['X-XSS-Protection'] = '1; mode=block'; + return $this; + } + + /** + * Set header Content-Security-Policy + * + * @access public + * @param array $policies + * @return $this + */ + public function withContentSecurityPolicy(array $policies = array()) + { + $values = ''; + + foreach ($policies as $policy => $acl) { + $values .= $policy.' '.trim($acl).'; '; + } + + $this->withHeader('Content-Security-Policy', $values); + return $this; + } + + /** + * Set header X-Frame-Options + * + * @access public + * @return $this + */ + public function withXframe() + { + $this->withHeader('X-Frame-Options', 'DENY'); + return $this; + } + + /** + * Set header Strict-Transport-Security (only if we use HTTPS) + * + * @access public + * @return $this + */ + public function withStrictTransportSecurity() + { + if ($this->request->isHTTPS()) { + $this->withHeader('Strict-Transport-Security', 'max-age=31536000'); + } + + return $this; + } + + /** + * Set HTTP response body + * + * @access public + * @param string $body + * @return $this + */ + public function withBody($body) + { + $this->httpBody = $body; + return $this; + } + /** * Send headers to cache a resource * * @access public * @param integer $duration * @param string $etag + * @return $this */ - public function cache($duration, $etag = '') + public function withCache($duration, $etag = '') { - header('Pragma: cache'); - header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT'); - header('Cache-Control: public, max-age=' . $duration); + $this + ->withHeader('Pragma', 'cache') + ->withHeader('Expires', gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT') + ->withHeader('Cache-Control', 'public, max-age=' . $duration) + ; if ($etag) { - header('ETag: "' . $etag . '"'); + $this->withHeader('ETag', '"' . $etag . '"'); } + + return $this; } /** * Send no cache headers * * @access public + * @return $this */ - public function nocache() + public function withoutCache() { - header('Pragma: no-cache'); - header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); - - // Use no-store due to a Chrome bug: https://code.google.com/p/chromium/issues/detail?id=28035 - header('Cache-Control: no-store, must-revalidate'); - } - - /** - * Send a custom Content-Type header - * - * @access public - * @param string $mimetype Mime-type - */ - public function contentType($mimetype) - { - header('Content-Type: '.$mimetype); + $this->withHeader('Pragma', 'no-cache'); + $this->withHeader('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT'); + return $this; } /** * Force the browser to download an attachment * * @access public - * @param string $filename File name + * @param string $filename + * @return $this */ - public function forceDownload($filename) + public function withFileDownload($filename) { - header('Content-Disposition: attachment; filename="'.$filename.'"'); - header('Content-Transfer-Encoding: binary'); - header('Content-Type: application/octet-stream'); + $this->withHeader('Content-Disposition', 'attachment; filename="'.$filename.'"'); + $this->withHeader('Content-Transfer-Encoding', 'binary'); + $this->withHeader('Content-Type', 'application/octet-stream'); + return $this; + } + + /** + * Send headers and body + * + * @access public + */ + public function send() + { + $this->responseSent = true; + + if ($this->httpStatusCode !== 200) { + header('Status: '.$this->httpStatusCode); + header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$this->httpStatusCode); + } + + foreach ($this->httpHeaders as $header => $value) { + header($header.': '.$value); + } + + if (! empty($this->httpBody)) { + echo $this->httpBody; + } } /** * Send a custom HTTP status code * * @access public - * @param integer $status_code HTTP status code + * @param integer $statusCode */ - public function status($status_code) + public function status($statusCode) { - header('Status: '.$status_code); - header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code); + $this->withStatusCode($statusCode); + $this->send(); } /** @@ -91,204 +237,149 @@ class Response extends Base public function redirect($url, $self = false) { if ($this->request->isAjax()) { - header('X-Ajax-Redirect: '.($self ? 'self' : $url)); + $this->withHeader('X-Ajax-Redirect', $self ? 'self' : $url); } else { - header('Location: '.$url); + $this->withHeader('Location', $url); } - exit; - } - - /** - * Send a CSV response - * - * @access public - * @param array $data Data to serialize in csv - * @param integer $status_code HTTP status code - */ - public function csv(array $data, $status_code = 200) - { - $this->status($status_code); - $this->nocache(); - - header('Content-Type: text/csv'); - Csv::output($data); - exit; - } - - /** - * Send a Json response - * - * @access public - * @param array $data Data to serialize in json - * @param integer $status_code HTTP status code - */ - public function json(array $data, $status_code = 200) - { - $this->status($status_code); - $this->nocache(); - header('Content-Type: application/json'); - echo json_encode($data); - exit; - } - - /** - * Send a text response - * - * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code - */ - public function text($data, $status_code = 200) - { - $this->status($status_code); - $this->nocache(); - header('Content-Type: text/plain; charset=utf-8'); - echo $data; - exit; + $this->send(); } /** * Send a HTML response * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param string $data + * @param integer $statusCode */ - public function html($data, $status_code = 200) + public function html($data, $statusCode = 200) { - $this->status($status_code); - $this->nocache(); - header('Content-Type: text/html; charset=utf-8'); - echo $data; - exit; + $this->withStatusCode($statusCode); + $this->withContentType('text/html; charset=utf-8'); + $this->withBody($data); + $this->send(); + } + + /** + * Send a text response + * + * @access public + * @param string $data + * @param integer $statusCode + */ + public function text($data, $statusCode = 200) + { + $this->withStatusCode($statusCode); + $this->withContentType('text/plain; charset=utf-8'); + $this->withBody($data); + $this->send(); + } + + /** + * Send a CSV response + * + * @access public + * @param array $data Data to serialize in csv + */ + public function csv(array $data) + { + $this->withoutCache(); + $this->withContentType('text/csv; charset=utf-8'); + $this->send(); + Csv::output($data); + } + + /** + * Send a Json response + * + * @access public + * @param array $data Data to serialize in json + * @param integer $statusCode HTTP status code + */ + public function json(array $data, $statusCode = 200) + { + $this->withStatusCode($statusCode); + $this->withContentType('application/json'); + $this->withoutCache(); + $this->withBody(json_encode($data)); + $this->send(); } /** * Send a XML response * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param string $data + * @param integer $statusCode */ - public function xml($data, $status_code = 200) + public function xml($data, $statusCode = 200) { - $this->status($status_code); - $this->nocache(); - header('Content-Type: text/xml; charset=utf-8'); - echo $data; - exit; + $this->withStatusCode($statusCode); + $this->withContentType('text/xml; charset=utf-8'); + $this->withoutCache(); + $this->withBody($data); + $this->send(); } /** * Send a javascript response * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param string $data + * @param integer $statusCode */ - public function js($data, $status_code = 200) + public function js($data, $statusCode = 200) { - $this->status($status_code); - - header('Content-Type: text/javascript; charset=utf-8'); - echo $data; - - exit; + $this->withStatusCode($statusCode); + $this->withContentType('text/javascript; charset=utf-8'); + $this->withBody($data); + $this->send(); } /** * Send a css response * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param string $data + * @param integer $statusCode */ - public function css($data, $status_code = 200) + public function css($data, $statusCode = 200) { - $this->status($status_code); - - header('Content-Type: text/css; charset=utf-8'); - echo $data; - - exit; + $this->withStatusCode($statusCode); + $this->withContentType('text/css; charset=utf-8'); + $this->withBody($data); + $this->send(); } /** * Send a binary response * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param string $data + * @param integer $statusCode */ - public function binary($data, $status_code = 200) + public function binary($data, $statusCode = 200) { - $this->status($status_code); - $this->nocache(); - header('Content-Transfer-Encoding: binary'); - header('Content-Type: application/octet-stream'); - echo $data; - exit; + $this->withStatusCode($statusCode); + $this->withoutCache(); + $this->withHeader('Content-Transfer-Encoding', 'binary'); + $this->withContentType('application/octet-stream'); + $this->withBody($data); + $this->send(); } /** - * Send the security header: Content-Security-Policy + * Send a iCal response * * @access public - * @param array $policies CSP rules + * @param string $data + * @param integer $statusCode */ - public function csp(array $policies = array()) + public function ical($data, $statusCode = 200) { - $values = ''; - - foreach ($policies as $policy => $acl) { - $values .= $policy.' '.trim($acl).'; '; - } - - header('Content-Security-Policy: '.$values); - } - - /** - * Send the security header: X-Content-Type-Options - * - * @access public - */ - public function nosniff() - { - header('X-Content-Type-Options: nosniff'); - } - - /** - * Send the security header: X-XSS-Protection - * - * @access public - */ - public function xss() - { - header('X-XSS-Protection: 1; mode=block'); - } - - /** - * Send the security header: Strict-Transport-Security (only if we use HTTPS) - * - * @access public - */ - public function hsts() - { - if ($this->request->isHTTPS()) { - header('Strict-Transport-Security: max-age=31536000'); - } - } - - /** - * Send the security header: X-Frame-Options (deny by default) - * - * @access public - * @param string $mode Frame option mode - * @param array $urls Allowed urls for the given mode - */ - public function xframe($mode = 'DENY', array $urls = array()) - { - header('X-Frame-Options: '.$mode.' '.implode(' ', $urls)); + $this->withStatusCode($statusCode); + $this->withContentType('text/calendar; charset=utf-8'); + $this->withBody($data); + $this->send(); } } diff --git a/sources/app/Core/Http/Route.php b/sources/app/Core/Http/Route.php index 7836146..9b45b72 100644 --- a/sources/app/Core/Http/Route.php +++ b/sources/app/Core/Http/Route.php @@ -119,8 +119,8 @@ class Route extends Base } return array( - 'controller' => 'app', - 'action' => 'index', + 'controller' => 'DashboardController', + 'action' => 'show', 'plugin' => '', ); } diff --git a/sources/app/Core/Http/Router.php b/sources/app/Core/Http/Router.php index 0fe80ec..4de276a 100644 --- a/sources/app/Core/Http/Router.php +++ b/sources/app/Core/Http/Router.php @@ -2,7 +2,6 @@ namespace Kanboard\Core\Http; -use RuntimeException; use Kanboard\Core\Base; /** @@ -13,13 +12,16 @@ use Kanboard\Core\Base; */ class Router extends Base { + const DEFAULT_CONTROLLER = 'DashboardController'; + const DEFAULT_METHOD = 'show'; + /** * Plugin name * * @access private * @var string */ - private $plugin = ''; + private $currentPluginName = ''; /** * Controller @@ -27,7 +29,7 @@ class Router extends Base * @access private * @var string */ - private $controller = ''; + private $currentControllerName = ''; /** * Action @@ -35,7 +37,7 @@ class Router extends Base * @access private * @var string */ - private $action = ''; + private $currentActionName = ''; /** * Get plugin name @@ -45,7 +47,7 @@ class Router extends Base */ public function getPlugin() { - return $this->plugin; + return $this->currentPluginName; } /** @@ -56,7 +58,7 @@ class Router extends Base */ public function getController() { - return $this->controller; + return $this->currentControllerName; } /** @@ -67,7 +69,7 @@ class Router extends Base */ public function getAction() { - return $this->action; + return $this->currentActionName; } /** @@ -109,11 +111,9 @@ class Router extends Base $plugin = $route['plugin']; } - $this->controller = ucfirst($this->sanitize($controller, 'app')); - $this->action = $this->sanitize($action, 'index'); - $this->plugin = ucfirst($this->sanitize($plugin)); - - return $this->executeAction(); + $this->currentControllerName = ucfirst($this->sanitize($controller, self::DEFAULT_CONTROLLER)); + $this->currentActionName = $this->sanitize($action, self::DEFAULT_METHOD); + $this->currentPluginName = ucfirst($this->sanitize($plugin)); } /** @@ -128,42 +128,4 @@ class Router extends Base { return preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $value : $default; } - - /** - * Execute controller action - * - * @access private - */ - private function executeAction() - { - $class = $this->getControllerClassName(); - - if (! class_exists($class)) { - throw new RuntimeException('Controller not found'); - } - - if (! method_exists($class, $this->action)) { - throw new RuntimeException('Action not implemented'); - } - - $instance = new $class($this->container); - $instance->beforeAction(); - $instance->{$this->action}(); - return $instance; - } - - /** - * Get controller class name - * - * @access private - * @return string - */ - private function getControllerClassName() - { - if ($this->plugin !== '') { - return '\Kanboard\Plugin\\'.$this->plugin.'\Controller\\'.$this->controller; - } - - return '\Kanboard\Controller\\'.$this->controller; - } } diff --git a/sources/app/Core/Ldap/Group.php b/sources/app/Core/Ldap/Group.php index 634d47e..e1f60ab 100644 --- a/sources/app/Core/Ldap/Group.php +++ b/sources/app/Core/Ldap/Group.php @@ -39,12 +39,11 @@ class Group * @access public * @param Client $client * @param string $query - * @return array + * @return LdapGroupProvider[] */ public static function getGroups(Client $client, $query) { - $className = get_called_class(); - $self = new $className(new Query($client)); + $self = new static(new Query($client)); return $self->find($query); } @@ -111,7 +110,7 @@ class Group throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_GROUP_ATTRIBUTE_NAME'); } - return LDAP_GROUP_ATTRIBUTE_NAME; + return strtolower(LDAP_GROUP_ATTRIBUTE_NAME); } /** diff --git a/sources/app/Core/Ldap/Query.php b/sources/app/Core/Ldap/Query.php index 7c1524c..0f9abb5 100644 --- a/sources/app/Core/Ldap/Query.php +++ b/sources/app/Core/Ldap/Query.php @@ -66,6 +66,10 @@ class Query $this->entries = $entries; + if (DEBUG && $this->client->hasLogger()) { + $this->client->getLogger()->debug('NbEntries='.$entries['count']); + } + return $this; } diff --git a/sources/app/Core/Ldap/User.php b/sources/app/Core/Ldap/User.php index d23ec07..4bc1f5f 100644 --- a/sources/app/Core/Ldap/User.php +++ b/sources/app/Core/Ldap/User.php @@ -22,15 +22,25 @@ class User */ protected $query; + /** + * LDAP Group object + * + * @access protected + * @var Group + */ + protected $group; + /** * Constructor * * @access public - * @param Query $query + * @param Query $query + * @param Group $group */ - public function __construct(Query $query) + public function __construct(Query $query, Group $group = null) { $this->query = $query; + $this->group = $group; } /** @@ -44,7 +54,7 @@ class User */ public static function getUser(Client $client, $username) { - $self = new static(new Query($client)); + $self = new static(new Query($client), new Group(new Query($client))); return $self->find($self->getLdapUserPattern($username)); } @@ -53,7 +63,7 @@ class User * * @access public * @param string $query - * @return null|LdapUserProvider + * @return LdapUserProvider */ public function find($query) { @@ -67,6 +77,62 @@ class User return $user; } + /** + * Get user groupIds (DN) + * + * 1) If configured, use memberUid and posixGroup + * 2) Otherwise, use memberOf + * + * @access protected + * @param Entry $entry + * @param string $username + * @return string[] + */ + protected function getGroups(Entry $entry, $username) + { + $groupIds = array(); + + if (! empty($username) && $this->group !== null && $this->hasGroupUserFilter()) { + $groups = $this->group->find(sprintf($this->getGroupUserFilter(), $username)); + + foreach ($groups as $group) { + $groupIds[] = $group->getExternalId(); + } + } else { + $groupIds = $entry->getAll($this->getAttributeGroup()); + } + + return $groupIds; + } + + /** + * Get role from LDAP groups + * + * Note: Do not touch the current role if groups are not configured + * + * @access protected + * @param string[] $groupIds + * @return string + */ + protected function getRole(array $groupIds) + { + if (! $this->hasGroupsConfigured()) { + return null; + } + + foreach ($groupIds as $groupId) { + $groupId = strtolower($groupId); + + if ($groupId === strtolower($this->getGroupAdminDn())) { + return Role::APP_ADMIN; + } elseif ($groupId === strtolower($this->getGroupManagerDn())) { + return Role::APP_MANAGER; + } + } + + return Role::APP_USER; + } + /** * Build user profile * @@ -76,21 +142,18 @@ class User protected function build() { $entry = $this->query->getEntries()->getFirstEntry(); - $role = Role::APP_USER; - - if ($entry->hasValue($this->getAttributeGroup(), $this->getGroupAdminDn())) { - $role = Role::APP_ADMIN; - } elseif ($entry->hasValue($this->getAttributeGroup(), $this->getGroupManagerDn())) { - $role = Role::APP_MANAGER; - } + $username = $entry->getFirstValue($this->getAttributeUsername()); + $groupIds = $this->getGroups($entry, $username); return new LdapUserProvider( $entry->getDn(), - $entry->getFirstValue($this->getAttributeUsername()), + $username, $entry->getFirstValue($this->getAttributeName()), $entry->getFirstValue($this->getAttributeEmail()), - $role, - $entry->getAll($this->getAttributeGroup()) + $this->getRole($groupIds), + $groupIds, + $entry->getFirstValue($this->getAttributePhoto()), + $entry->getFirstValue($this->getAttributeLanguage()) ); } @@ -109,6 +172,8 @@ class User $this->getAttributeName(), $this->getAttributeEmail(), $this->getAttributeGroup(), + $this->getAttributePhoto(), + $this->getAttributeLanguage(), ))); } @@ -124,7 +189,7 @@ class User throw new LogicException('LDAP username attribute empty, check the parameter LDAP_USER_ATTRIBUTE_USERNAME'); } - return LDAP_USER_ATTRIBUTE_USERNAME; + return strtolower(LDAP_USER_ATTRIBUTE_USERNAME); } /** @@ -139,7 +204,7 @@ class User throw new LogicException('LDAP full name attribute empty, check the parameter LDAP_USER_ATTRIBUTE_FULLNAME'); } - return LDAP_USER_ATTRIBUTE_FULLNAME; + return strtolower(LDAP_USER_ATTRIBUTE_FULLNAME); } /** @@ -154,18 +219,73 @@ class User throw new LogicException('LDAP email attribute empty, check the parameter LDAP_USER_ATTRIBUTE_EMAIL'); } - return LDAP_USER_ATTRIBUTE_EMAIL; + return strtolower(LDAP_USER_ATTRIBUTE_EMAIL); } /** - * Get LDAP account memberof attribute + * Get LDAP account memberOf attribute * * @access public * @return string */ public function getAttributeGroup() { - return LDAP_USER_ATTRIBUTE_GROUPS; + return strtolower(LDAP_USER_ATTRIBUTE_GROUPS); + } + + /** + * Get LDAP profile photo attribute + * + * @access public + * @return string + */ + public function getAttributePhoto() + { + return strtolower(LDAP_USER_ATTRIBUTE_PHOTO); + } + + /** + * Get LDAP language attribute + * + * @access public + * @return string + */ + public function getAttributeLanguage() + { + return strtolower(LDAP_USER_ATTRIBUTE_LANGUAGE); + } + + /** + * Get LDAP Group User filter + * + * @access public + * @return string + */ + public function getGroupUserFilter() + { + return LDAP_GROUP_USER_FILTER; + } + + /** + * Return true if LDAP Group User filter is defined + * + * @access public + * @return string + */ + public function hasGroupUserFilter() + { + return $this->getGroupUserFilter() !== '' && $this->getGroupUserFilter() !== null; + } + + /** + * Return true if LDAP Group mapping are configured + * + * @access public + * @return boolean + */ + public function hasGroupsConfigured() + { + return $this->getGroupAdminDn() || $this->getGroupManagerDn(); } /** @@ -176,7 +296,7 @@ class User */ public function getGroupAdminDn() { - return LDAP_GROUP_ADMIN_DN; + return strtolower(LDAP_GROUP_ADMIN_DN); } /** diff --git a/sources/app/Core/Lexer.php b/sources/app/Core/Lexer.php deleted file mode 100644 index df2d90a..0000000 --- a/sources/app/Core/Lexer.php +++ /dev/null @@ -1,161 +0,0 @@ - 'T_ASSIGNEE', - "/^(color:)/" => 'T_COLOR', - "/^(due:)/" => 'T_DUE', - "/^(updated:)/" => 'T_UPDATED', - "/^(modified:)/" => 'T_UPDATED', - "/^(created:)/" => 'T_CREATED', - "/^(status:)/" => 'T_STATUS', - "/^(description:)/" => 'T_DESCRIPTION', - "/^(category:)/" => 'T_CATEGORY', - "/^(column:)/" => 'T_COLUMN', - "/^(project:)/" => 'T_PROJECT', - "/^(swimlane:)/" => 'T_SWIMLANE', - "/^(ref:)/" => 'T_REFERENCE', - "/^(reference:)/" => 'T_REFERENCE', - "/^(link:)/" => 'T_LINK', - "/^(\s+)/" => 'T_WHITESPACE', - '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE', - '/^(yesterday|tomorrow|today)/' => 'T_DATE', - '/^("(.*?)")/' => 'T_STRING', - "/^(\w+)/" => 'T_STRING', - "/^(#\d+)/" => 'T_STRING', - ); - - /** - * Tokenize input string - * - * @access public - * @param string $input - * @return array - */ - public function tokenize($input) - { - $tokens = array(); - $this->offset = 0; - - while (isset($input[$this->offset])) { - $result = $this->match(substr($input, $this->offset)); - - if ($result === false) { - return array(); - } - - $tokens[] = $result; - } - - return $tokens; - } - - /** - * Find a token that match and move the offset - * - * @access public - * @param string $string - * @return array|boolean - */ - public function match($string) - { - foreach ($this->tokenMap as $pattern => $name) { - if (preg_match($pattern, $string, $matches)) { - $this->offset += strlen($matches[1]); - - return array( - 'match' => trim($matches[1], '"'), - 'token' => $name, - ); - } - } - - return false; - } - - /** - * Change the output of tokenizer to be easily parsed by the database filter - * - * Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title'] - * - * @access public - * @param array $tokens - * @return array - */ - public function map(array $tokens) - { - $map = array( - 'T_TITLE' => '', - ); - - while (false !== ($token = current($tokens))) { - switch ($token['token']) { - case 'T_ASSIGNEE': - case 'T_COLOR': - case 'T_CATEGORY': - case 'T_COLUMN': - case 'T_PROJECT': - case 'T_SWIMLANE': - case 'T_LINK': - $next = next($tokens); - - if ($next !== false && $next['token'] === 'T_STRING') { - $map[$token['token']][] = $next['match']; - } - - break; - - case 'T_STATUS': - case 'T_DUE': - case 'T_UPDATED': - case 'T_CREATED': - case 'T_DESCRIPTION': - case 'T_REFERENCE': - $next = next($tokens); - - if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) { - $map[$token['token']] = $next['match']; - } - - break; - - default: - $map['T_TITLE'] .= $token['match']; - break; - } - - next($tokens); - } - - $map['T_TITLE'] = trim($map['T_TITLE']); - - if (empty($map['T_TITLE'])) { - unset($map['T_TITLE']); - } - - return $map; - } -} diff --git a/sources/app/Core/Mail/Client.php b/sources/app/Core/Mail/Client.php index 641b6ab..ee3bdea 100644 --- a/sources/app/Core/Mail/Client.php +++ b/sources/app/Core/Mail/Client.php @@ -2,6 +2,7 @@ namespace Kanboard\Core\Mail; +use Kanboard\Job\EmailJob; use Pimple\Container; use Kanboard\Core\Base; @@ -46,25 +47,31 @@ class Client extends Base public function send($email, $name, $subject, $html) { if (! empty($email)) { - $this->logger->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); - - $start_time = microtime(true); - $author = 'Kanboard'; - - if ($this->userSession->isLogged()) { - $author = e('%s via Kanboard', $this->helper->user->getFullname()); - } - - $this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author); - - if (DEBUG) { - $this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); - } + $this->queueManager->push(EmailJob::getInstance($this->container) + ->withParams($email, $name, $subject, $html, $this->getAuthor()) + ); } return $this; } + /** + * Get email author + * + * @access public + * @return string + */ + public function getAuthor() + { + $author = 'Kanboard'; + + if ($this->userSession->isLogged()) { + $author = e('%s via Kanboard', $this->helper->user->getFullname()); + } + + return $author; + } + /** * Get mail transport instance * @@ -95,4 +102,16 @@ class Client extends Base return $this; } + + /** + * Return the list of registered transports + * + * @access public + * @return array + */ + public function getAvailableTransports() + { + $availableTransports = $this->transports->keys(); + return array_combine($availableTransports, $availableTransports); + } } diff --git a/sources/app/Core/Mail/Transport/Mail.php b/sources/app/Core/Mail/Transport/Mail.php index aff3ee2..d27925f 100644 --- a/sources/app/Core/Mail/Transport/Mail.php +++ b/sources/app/Core/Mail/Transport/Mail.php @@ -32,7 +32,7 @@ class Mail extends Base implements ClientInterface try { $message = Swift_Message::newInstance() ->setSubject($subject) - ->setFrom(array(MAIL_FROM => $author)) + ->setFrom(array($this->helper->mail->getMailSenderAddress() => $author)) ->setBody($html, 'text/html') ->setTo(array($email => $name)); diff --git a/sources/app/Core/Markdown.php b/sources/app/Core/Markdown.php index 827fd0d..b5abe5e 100644 --- a/sources/app/Core/Markdown.php +++ b/sources/app/Core/Markdown.php @@ -15,12 +15,12 @@ use Pimple\Container; class Markdown extends Parsedown { /** - * Link params for tasks + * Task links generated will use the project token instead * * @access private - * @var array + * @var boolean */ - private $link = array(); + private $isPublicLink = false; /** * Container @@ -35,11 +35,11 @@ class Markdown extends Parsedown * * @access public * @param Container $container - * @param array $link + * @param boolean $isPublicLink */ - public function __construct(Container $container, array $link) + public function __construct(Container $container, $isPublicLink) { - $this->link = $link; + $this->isPublicLink = $isPublicLink; $this->container = $container; $this->InlineTypes['#'][] = 'TaskLink'; $this->InlineTypes['@'][] = 'UserLink'; @@ -53,26 +53,26 @@ class Markdown extends Parsedown * * @access public * @param array $Excerpt - * @return array + * @return array|null */ protected function inlineTaskLink(array $Excerpt) { - if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) { - $url = $this->container['helper']->url->href( - $this->link['controller'], - $this->link['action'], - $this->link['params'] + array('task_id' => $matches[1]) - ); + if (preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) { + $link = $this->buildTaskLink($matches[1]); - return array( - 'extent' => strlen($matches[0]), - 'element' => array( - 'name' => 'a', - 'text' => $matches[0], - 'attributes' => array('href' => $url) - ), - ); + if (! empty($link)) { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[0], + 'attributes' => array('href' => $link), + ), + ); + } } + + return null; } /** @@ -82,15 +82,15 @@ class Markdown extends Parsedown * * @access public * @param array $Excerpt - * @return array + * @return array|null */ protected function inlineUserLink(array $Excerpt) { - if (preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) { - $user_id = $this->container['user']->getIdByUsername($matches[1]); + if (! $this->isPublicLink && preg_match('/^@([^\s]+)/', $Excerpt['text'], $matches)) { + $user_id = $this->container['userModel']->getIdByUsername($matches[1]); if (! empty($user_id)) { - $url = $this->container['helper']->url->href('user', 'profile', array('user_id' => $user_id)); + $url = $this->container['helper']->url->href('UserViewController', 'profile', array('user_id' => $user_id)); return array( 'extent' => strlen($matches[0]), @@ -102,5 +102,40 @@ class Markdown extends Parsedown ); } } + + return null; + } + + /** + * Build task link + * + * @access private + * @param integer $task_id + * @return string + */ + private function buildTaskLink($task_id) + { + if ($this->isPublicLink) { + $token = $this->container['memoryCache']->proxy($this->container['taskFinderModel'], 'getProjectToken', $task_id); + + if (! empty($token)) { + return $this->container['helper']->url->href( + 'TaskViewController', + 'readonly', + array( + 'token' => $token, + 'task_id' => $task_id, + ) + ); + } + + return ''; + } + + return $this->container['helper']->url->href( + 'TaskViewController', + 'show', + array('task_id' => $task_id) + ); } } diff --git a/sources/app/Notification/NotificationInterface.php b/sources/app/Core/Notification/NotificationInterface.php similarity index 89% rename from sources/app/Notification/NotificationInterface.php rename to sources/app/Core/Notification/NotificationInterface.php index 8431a77..d336983 100644 --- a/sources/app/Notification/NotificationInterface.php +++ b/sources/app/Core/Notification/NotificationInterface.php @@ -1,11 +1,11 @@ container; - $this->container['dispatcher']->addListener($event, function () use ($container, $callback) { + $this->dispatcher->addListener($event, function () use ($container, $callback) { call_user_func($callback, $container); }); } @@ -70,7 +70,7 @@ abstract class Base extends \Kanboard\Core\Base /** * Get plugin name * - * This method should be overrided by your Plugin class + * This method should be overridden by your Plugin class * * @access public * @return string @@ -83,7 +83,7 @@ abstract class Base extends \Kanboard\Core\Base /** * Get plugin description * - * This method should be overrided by your Plugin class + * This method should be overridden by your Plugin class * * @access public * @return string @@ -96,7 +96,7 @@ abstract class Base extends \Kanboard\Core\Base /** * Get plugin author * - * This method should be overrided by your Plugin class + * This method should be overridden by your Plugin class * * @access public * @return string @@ -109,7 +109,7 @@ abstract class Base extends \Kanboard\Core\Base /** * Get plugin version * - * This method should be overrided by your Plugin class + * This method should be overridden by your Plugin class * * @access public * @return string @@ -122,7 +122,7 @@ abstract class Base extends \Kanboard\Core\Base /** * Get plugin homepage * - * This method should be overrided by your Plugin class + * This method should be overridden by your Plugin class * * @access public * @return string diff --git a/sources/app/Core/Plugin/Directory.php b/sources/app/Core/Plugin/Directory.php new file mode 100644 index 0000000..21f11ca --- /dev/null +++ b/sources/app/Core/Plugin/Directory.php @@ -0,0 +1,56 @@ +httpClient->getJson($url); + $plugins = array_filter($plugins, array($this, 'isCompatible')); + $plugins = array_filter($plugins, array($this, 'isInstallable')); + return $plugins; + } + + /** + * Filter plugins + * + * @param array $plugin + * @param string $appVersion + * @return bool + */ + public function isCompatible(array $plugin, $appVersion = APP_VERSION) + { + if (strpos($appVersion, 'master') !== false) { + return true; + } + + return $plugin['compatible_version'] === $appVersion; + } + + /** + * Filter plugins + * + * @param array $plugin + * @return bool + */ + public function isInstallable(array $plugin) + { + return $plugin['remote_install']; + } +} diff --git a/sources/app/Core/Plugin/Hook.php b/sources/app/Core/Plugin/Hook.php index a3bcd91..ade6915 100644 --- a/sources/app/Core/Plugin/Hook.php +++ b/sources/app/Core/Plugin/Hook.php @@ -5,8 +5,8 @@ namespace Kanboard\Core\Plugin; /** * Plugin Hooks Handler * - * @package plugin - * @author Frederic Guillot + * @package Kanboard\Core\Plugin + * @author Frederic Guillot */ class Hook { diff --git a/sources/app/Core/Plugin/Installer.php b/sources/app/Core/Plugin/Installer.php new file mode 100644 index 0000000..48c4d97 --- /dev/null +++ b/sources/app/Core/Plugin/Installer.php @@ -0,0 +1,162 @@ +downloadPluginArchive($archiveUrl); + + if (! $zip->extractTo(PLUGINS_DIR)) { + $this->cleanupArchive($zip); + throw new PluginInstallerException(t('Unable to extract plugin archive.')); + } + + $this->cleanupArchive($zip); + } + + /** + * Uninstall a plugin + * + * @access public + * @param string $pluginId + * @throws PluginInstallerException + */ + public function uninstall($pluginId) + { + $pluginFolder = PLUGINS_DIR.DIRECTORY_SEPARATOR.basename($pluginId); + + if (! file_exists($pluginFolder)) { + throw new PluginInstallerException(t('Plugin not found.')); + } + + if (! is_writable($pluginFolder)) { + throw new PluginInstallerException(e('You don\'t have the permission to remove this plugin.')); + } + + $this->removeAllDirectories($pluginFolder); + } + + /** + * Update a plugin + * + * @access public + * @param string $archiveUrl + * @throws PluginInstallerException + */ + public function update($archiveUrl) + { + $zip = $this->downloadPluginArchive($archiveUrl); + + $firstEntry = $zip->statIndex(0); + $this->uninstall($firstEntry['name']); + + if (! $zip->extractTo(PLUGINS_DIR)) { + $this->cleanupArchive($zip); + throw new PluginInstallerException(t('Unable to extract plugin archive.')); + } + + $this->cleanupArchive($zip); + } + + /** + * Download archive from URL + * + * @access protected + * @param string $archiveUrl + * @return ZipArchive + * @throws PluginInstallerException + */ + protected function downloadPluginArchive($archiveUrl) + { + $zip = new ZipArchive(); + $archiveData = $this->httpClient->get($archiveUrl); + $archiveFile = tempnam(sys_get_temp_dir(), 'kb_plugin'); + + if (empty($archiveData)) { + unlink($archiveFile); + throw new PluginInstallerException(t('Unable to download plugin archive.')); + } + + if (file_put_contents($archiveFile, $archiveData) === false) { + unlink($archiveFile); + throw new PluginInstallerException(t('Unable to write temporary file for plugin.')); + } + + if ($zip->open($archiveFile) !== true) { + unlink($archiveFile); + throw new PluginInstallerException(t('Unable to open plugin archive.')); + } + + if ($zip->numFiles === 0) { + unlink($archiveFile); + throw new PluginInstallerException(t('There is no file in the plugin archive.')); + } + + return $zip; + } + + /** + * Remove archive file + * + * @access protected + * @param ZipArchive $zip + */ + protected function cleanupArchive(ZipArchive $zip) + { + unlink($zip->filename); + $zip->close(); + } + + /** + * Remove recursively a directory + * + * @access protected + * @param string $directory + */ + protected function removeAllDirectories($directory) + { + $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); + $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($files as $file) { + if ($file->isDir()) { + rmdir($file->getRealPath()); + } else { + unlink($file->getRealPath()); + } + } + + rmdir($directory); + } +} diff --git a/sources/app/Core/Plugin/Loader.php b/sources/app/Core/Plugin/Loader.php index 775673d..f2f6add 100644 --- a/sources/app/Core/Plugin/Loader.php +++ b/sources/app/Core/Plugin/Loader.php @@ -2,34 +2,37 @@ namespace Kanboard\Core\Plugin; +use Composer\Autoload\ClassLoader; use DirectoryIterator; -use PDOException; use LogicException; -use RuntimeException; use Kanboard\Core\Tool; /** * Plugin Loader * - * @package plugin - * @author Frederic Guillot + * @package Kanboard\Core\Plugin + * @author Frederic Guillot */ class Loader extends \Kanboard\Core\Base { - /** - * Schema version table for plugins - * - * @var string - */ - const TABLE_SCHEMA = 'plugin_schema_versions'; - /** * Plugin instances * - * @access public + * @access protected * @var array */ - public $plugins = array(); + protected $plugins = array(); + + /** + * Get list of loaded plugins + * + * @access public + * @return Base[] + */ + public function getPlugins() + { + return $this->plugins; + } /** * Scan plugin folder and load plugins @@ -39,118 +42,72 @@ class Loader extends \Kanboard\Core\Base public function scan() { if (file_exists(PLUGINS_DIR)) { + $loader = new ClassLoader(); + $loader->addPsr4('Kanboard\Plugin\\', PLUGINS_DIR); + $loader->register(); + $dir = new DirectoryIterator(PLUGINS_DIR); - foreach ($dir as $fileinfo) { - if (! $fileinfo->isDot() && $fileinfo->isDir()) { - $plugin = $fileinfo->getFilename(); - $this->loadSchema($plugin); - $this->load($plugin); + foreach ($dir as $fileInfo) { + if ($fileInfo->isDir() && substr($fileInfo->getFilename(), 0, 1) !== '.') { + $pluginName = $fileInfo->getFilename(); + $this->loadSchema($pluginName); + $this->initializePlugin($pluginName, $this->loadPlugin($pluginName)); } } } } + /** + * Load plugin schema + * + * @access public + * @param string $pluginName + */ + public function loadSchema($pluginName) + { + if (SchemaHandler::hasSchema($pluginName)) { + $schemaHandler = new SchemaHandler($this->container); + $schemaHandler->loadSchema($pluginName); + } + } + /** * Load plugin * * @access public * @throws LogicException - * @param string $plugin + * @param string $pluginName + * @return Base */ - public function load($plugin) + public function loadPlugin($pluginName) { - $class = '\Kanboard\Plugin\\'.$plugin.'\\Plugin'; + $className = '\Kanboard\Plugin\\'.$pluginName.'\\Plugin'; - if (! class_exists($class)) { - throw new LogicException('Unable to load this plugin class '.$class); + if (! class_exists($className)) { + throw new LogicException('Unable to load this plugin class '.$className); } - $instance = new $class($this->container); - - Tool::buildDic($this->container, $instance->getClasses()); - - Tool::buildDICHelpers($this->container, $instance->getHelpers()); - - $instance->initialize(); - $this->plugins[] = $instance; + return new $className($this->container); } /** - * Load plugin schema + * Initialize plugin * * @access public - * @param string $plugin + * @param string $pluginName + * @param Base $plugin */ - public function loadSchema($plugin) + public function initializePlugin($pluginName, Base $plugin) { - $filename = PLUGINS_DIR.'/'.$plugin.'/Schema/'.ucfirst(DB_DRIVER).'.php'; - - if (file_exists($filename)) { - require_once($filename); - $this->migrateSchema($plugin); + if (method_exists($plugin, 'onStartup')) { + $this->dispatcher->addListener('app.bootstrap', array($plugin, 'onStartup')); } - } - /** - * Execute plugin schema migrations - * - * @access public - * @param string $plugin - */ - public function migrateSchema($plugin) - { - $last_version = constant('\Kanboard\Plugin\\'.$plugin.'\Schema\VERSION'); - $current_version = $this->getSchemaVersion($plugin); + Tool::buildDIC($this->container, $plugin->getClasses()); + Tool::buildDICHelpers($this->container, $plugin->getHelpers()); - try { - $this->db->startTransaction(); - $this->db->getDriver()->disableForeignKeys(); - - for ($i = $current_version + 1; $i <= $last_version; $i++) { - $function_name = '\Kanboard\Plugin\\'.$plugin.'\Schema\version_'.$i; - - if (function_exists($function_name)) { - call_user_func($function_name, $this->db->getConnection()); - } - } - - $this->db->getDriver()->enableForeignKeys(); - $this->db->closeTransaction(); - $this->setSchemaVersion($plugin, $i - 1); - } catch (PDOException $e) { - $this->db->cancelTransaction(); - $this->db->getDriver()->enableForeignKeys(); - throw new RuntimeException('Unable to migrate schema for the plugin: '.$plugin.' => '.$e->getMessage()); - } - } - - /** - * Get current plugin schema version - * - * @access public - * @param string $plugin - * @return integer - */ - public function getSchemaVersion($plugin) - { - return (int) $this->db->table(self::TABLE_SCHEMA)->eq('plugin', strtolower($plugin))->findOneColumn('version'); - } - - /** - * Save last plugin schema version - * - * @access public - * @param string $plugin - * @param integer $version - * @return boolean - */ - public function setSchemaVersion($plugin, $version) - { - $dictionary = array( - strtolower($plugin) => $version - ); - - return $this->db->getDriver()->upsert(self::TABLE_SCHEMA, 'plugin', 'version', $dictionary); + $plugin->initialize(); + $this->plugins[$pluginName] = $plugin; } } diff --git a/sources/app/Core/Plugin/PluginInstallerException.php b/sources/app/Core/Plugin/PluginInstallerException.php new file mode 100644 index 0000000..7d356c9 --- /dev/null +++ b/sources/app/Core/Plugin/PluginInstallerException.php @@ -0,0 +1,15 @@ +migrateSchema($pluginName); + } + + /** + * Execute plugin schema migrations + * + * @access public + * @param string $pluginName + */ + public function migrateSchema($pluginName) + { + $lastVersion = constant('\Kanboard\Plugin\\'.$pluginName.'\Schema\VERSION'); + $currentVersion = $this->getSchemaVersion($pluginName); + + try { + $this->db->startTransaction(); + $this->db->getDriver()->disableForeignKeys(); + + for ($i = $currentVersion + 1; $i <= $lastVersion; $i++) { + $functionName = '\Kanboard\Plugin\\'.$pluginName.'\Schema\version_'.$i; + + if (function_exists($functionName)) { + call_user_func($functionName, $this->db->getConnection()); + } + } + + $this->db->getDriver()->enableForeignKeys(); + $this->db->closeTransaction(); + $this->setSchemaVersion($pluginName, $i - 1); + } catch (PDOException $e) { + $this->db->cancelTransaction(); + $this->db->getDriver()->enableForeignKeys(); + throw new RuntimeException('Unable to migrate schema for the plugin: '.$pluginName.' => '.$e->getMessage()); + } + } + + /** + * Get current plugin schema version + * + * @access public + * @param string $plugin + * @return integer + */ + public function getSchemaVersion($plugin) + { + return (int) $this->db->table(self::TABLE_SCHEMA)->eq('plugin', strtolower($plugin))->findOneColumn('version'); + } + + /** + * Save last plugin schema version + * + * @access public + * @param string $plugin + * @param integer $version + * @return boolean + */ + public function setSchemaVersion($plugin, $version) + { + $dictionary = array( + strtolower($plugin) => $version + ); + + return $this->db->getDriver()->upsert(self::TABLE_SCHEMA, 'plugin', 'version', $dictionary); + } +} diff --git a/sources/app/Core/Queue/JobHandler.php b/sources/app/Core/Queue/JobHandler.php new file mode 100644 index 0000000..7ca3632 --- /dev/null +++ b/sources/app/Core/Queue/JobHandler.php @@ -0,0 +1,70 @@ + get_class($job), + 'params' => $job->getJobParams(), + 'user_id' => $this->userSession->getId(), + )); + } + + /** + * Execute a job + * + * @access public + * @param Job $job + */ + public function executeJob(Job $job) + { + $payload = $job->getBody(); + $className = $payload['class']; + $this->memoryCache->flush(); + $this->prepareJobSession($payload['user_id']); + + if (DEBUG) { + $this->logger->debug(__METHOD__.' Received job => '.$className.' ('.getmypid().')'); + } + + $worker = new $className($this->container); + call_user_func_array(array($worker, 'execute'), $payload['params']); + } + + /** + * Create the session for the job + * + * @access protected + * @param integer $user_id + */ + protected function prepareJobSession($user_id) + { + $session = array(); + $this->sessionStorage->setStorage($session); + + if ($user_id > 0) { + $user = $this->userModel->getById($user_id); + $this->userSession->initialize($user); + } + } +} diff --git a/sources/app/Core/Queue/QueueManager.php b/sources/app/Core/Queue/QueueManager.php new file mode 100644 index 0000000..f34cb22 --- /dev/null +++ b/sources/app/Core/Queue/QueueManager.php @@ -0,0 +1,71 @@ +queue = $queue; + return $this; + } + + /** + * Send a new job to the queue + * + * @access public + * @param BaseJob $job + * @return $this + */ + public function push(BaseJob $job) + { + if ($this->queue !== null) { + $this->queue->push(JobHandler::getInstance($this->container)->serializeJob($job)); + } else { + call_user_func_array(array($job, 'execute'), $job->getJobParams()); + } + + return $this; + } + + /** + * Wait for new jobs + * + * @access public + * @throws LogicException + */ + public function listen() + { + if ($this->queue === null) { + throw new LogicException('No Queue Driver defined!'); + } + + while ($job = $this->queue->pull()) { + JobHandler::getInstance($this->container)->executeJob($job); + $this->queue->completed($job); + } + } +} diff --git a/sources/app/Core/Session/SessionStorage.php b/sources/app/Core/Session/SessionStorage.php index 6e2f966..9e93602 100644 --- a/sources/app/Core/Session/SessionStorage.php +++ b/sources/app/Core/Session/SessionStorage.php @@ -22,6 +22,7 @@ namespace Kanboard\Core\Session; * @property bool $twoFactorBeforeCodeCalled * @property string $twoFactorSecret * @property string $oauthState + * @property int $smsTwoFactorSecret */ class SessionStorage { diff --git a/sources/app/Core/Template.php b/sources/app/Core/Template.php index 1874d44..d3ec26d 100644 --- a/sources/app/Core/Template.php +++ b/sources/app/Core/Template.php @@ -116,7 +116,7 @@ class Template } if ($plugin !== 'kanboard' && $plugin !== '') { - return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', '..', 'plugins', ucfirst($plugin), 'Template', $template.'.php')); + return implode(DIRECTORY_SEPARATOR, array(PLUGINS_DIR, ucfirst($plugin), 'Template', $template.'.php')); } return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Template', $template.'.php')); diff --git a/sources/app/Core/Tool.php b/sources/app/Core/Tool.php index 3423998..bfa6c95 100644 --- a/sources/app/Core/Tool.php +++ b/sources/app/Core/Tool.php @@ -12,26 +12,6 @@ use Pimple\Container; */ class Tool { - /** - * Get the mailbox hash from an email address - * - * @static - * @access public - * @param string $email - * @return string - */ - public static function getMailboxHash($email) - { - if (! strpos($email, '@') || ! strpos($email, '+')) { - return ''; - } - - list($local_part, ) = explode('@', $email); - list(, $identifier) = explode('+', $local_part); - - return $identifier; - } - /** * Build dependency injection container from an array * diff --git a/sources/app/Core/User/GroupSync.php b/sources/app/Core/User/GroupSync.php index 573acd4..d0bb647 100644 --- a/sources/app/Core/User/GroupSync.php +++ b/sources/app/Core/User/GroupSync.php @@ -16,16 +16,52 @@ class GroupSync extends Base * Synchronize group membership * * @access public - * @param integer $userId - * @param array $groupIds + * @param integer $userId + * @param array $externalGroupIds */ - public function synchronize($userId, array $groupIds) + public function synchronize($userId, array $externalGroupIds) { - foreach ($groupIds as $groupId) { - $group = $this->group->getByExternalId($groupId); + $userGroups = $this->groupMemberModel->getGroups($userId); + $this->addGroups($userId, $userGroups, $externalGroupIds); + $this->removeGroups($userId, $userGroups, $externalGroupIds); + } - if (! empty($group) && ! $this->groupMember->isMember($group['id'], $userId)) { - $this->groupMember->addUser($group['id'], $userId); + /** + * Add missing groups to the user + * + * @access protected + * @param integer $userId + * @param array $userGroups + * @param array $externalGroupIds + */ + protected function addGroups($userId, array $userGroups, array $externalGroupIds) + { + $userGroupIds = array_column($userGroups, 'external_id', 'external_id'); + + foreach ($externalGroupIds as $externalGroupId) { + if (! isset($userGroupIds[$externalGroupId])) { + $group = $this->groupModel->getByExternalId($externalGroupId); + + if (! empty($group)) { + $this->groupMemberModel->addUser($group['id'], $userId); + } + } + } + } + + /** + * Remove groups from the user + * + * @access protected + * @param integer $userId + * @param array $userGroups + * @param array $externalGroupIds + */ + protected function removeGroups($userId, array $userGroups, array $externalGroupIds) + { + foreach ($userGroups as $userGroup) { + if (! empty($userGroup['external_id']) && ! in_array($userGroup['external_id'], $externalGroupIds)) { + $this->groupMemberModel->removeUser($userGroup['id'], $userId); } } } diff --git a/sources/app/Core/User/UserProfile.php b/sources/app/Core/User/UserProfile.php index ef32580..8b9ebb7 100644 --- a/sources/app/Core/User/UserProfile.php +++ b/sources/app/Core/User/UserProfile.php @@ -3,6 +3,7 @@ namespace Kanboard\Core\User; use Kanboard\Core\Base; +use Kanboard\Event\UserProfileSyncEvent; /** * User Profile @@ -12,6 +13,8 @@ use Kanboard\Core\Base; */ class UserProfile extends Base { + const EVENT_USER_PROFILE_AFTER_SYNC = 'user_profile.after.sync'; + /** * Assign provider data to the local user * @@ -22,12 +25,12 @@ class UserProfile extends Base */ public function assign($userId, UserProviderInterface $user) { - $profile = $this->user->getById($userId); + $profile = $this->userModel->getById($userId); $values = UserProperty::filterProperties($profile, UserProperty::getProperties($user)); $values['id'] = $userId; - if ($this->user->update($values)) { + if ($this->userModel->update($values)) { $profile = array_merge($profile, $values); $this->userSession->initialize($profile); return true; @@ -46,7 +49,7 @@ class UserProfile extends Base public function initialize(UserProviderInterface $user) { if ($user->getInternalId()) { - $profile = $this->user->getById($user->getInternalId()); + $profile = $this->userModel->getById($user->getInternalId()); } elseif ($user->getExternalIdColumn() && $user->getExternalId()) { $profile = $this->userSync->synchronize($user); $this->groupSync->synchronize($profile['id'], $user->getExternalGroupIds()); @@ -54,6 +57,7 @@ class UserProfile extends Base if (! empty($profile) && $profile['is_active'] == 1) { $this->userSession->initialize($profile); + $this->dispatcher->dispatch(self::EVENT_USER_PROFILE_AFTER_SYNC, new UserProfileSyncEvent($profile, $user)); return true; } diff --git a/sources/app/Core/User/UserProperty.php b/sources/app/Core/User/UserProperty.php index f8b08a3..348bd7f 100644 --- a/sources/app/Core/User/UserProperty.php +++ b/sources/app/Core/User/UserProperty.php @@ -44,10 +44,14 @@ class UserProperty */ public static function filterProperties(array $profile, array $properties) { + $excludedProperties = array('username'); $values = array(); foreach ($properties as $property => $value) { - if (array_key_exists($property, $profile) && ! self::isNotEmptyValue($profile[$property])) { + if (self::isNotEmptyValue($value) && + ! in_array($property, $excludedProperties) && + array_key_exists($property, $profile) && + $value !== $profile[$property]) { $values[$property] = $value; } } diff --git a/sources/app/Core/User/UserSession.php b/sources/app/Core/User/UserSession.php index 0034c47..9c63f07 100644 --- a/sources/app/Core/User/UserSession.php +++ b/sources/app/Core/User/UserSession.php @@ -22,7 +22,7 @@ class UserSession extends Base public function refresh($user_id) { if ($this->getId() == $user_id) { - $this->initialize($this->user->getById($user_id)); + $this->initialize($this->userModel->getById($user_id)); } } diff --git a/sources/app/Core/User/UserSync.php b/sources/app/Core/User/UserSync.php index d450a0b..c2f8549 100644 --- a/sources/app/Core/User/UserSync.php +++ b/sources/app/Core/User/UserSync.php @@ -21,7 +21,7 @@ class UserSync extends Base */ public function synchronize(UserProviderInterface $user) { - $profile = $this->user->getByExternalId($user->getExternalIdColumn(), $user->getExternalId()); + $profile = $this->userModel->getByExternalId($user->getExternalIdColumn(), $user->getExternalId()); $properties = UserProperty::getProperties($user); if (! empty($profile)) { @@ -47,7 +47,7 @@ class UserSync extends Base if (! empty($values)) { $values['id'] = $profile['id']; - $result = $this->user->update($values); + $result = $this->userModel->update($values); return $result ? array_merge($profile, $properties) : $profile; } @@ -64,13 +64,13 @@ class UserSync extends Base */ private function createUser(UserProviderInterface $user, array $properties) { - $id = $this->user->create($properties); + $userId = $this->userModel->create($properties); - if ($id === false) { + if ($userId === false) { $this->logger->error('Unable to create user profile: '.$user->getExternalId()); return array(); } - return $this->user->getById($id); + return $this->userModel->getById($userId); } } diff --git a/sources/app/Event/UserProfileSyncEvent.php b/sources/app/Event/UserProfileSyncEvent.php new file mode 100644 index 0000000..c02e1d8 --- /dev/null +++ b/sources/app/Event/UserProfileSyncEvent.php @@ -0,0 +1,64 @@ +profile = $profile; + $this->user = $user; + } + + /** + * Get user profile + * + * @access public + * @return array + */ + public function getProfile() + { + return $this->profile; + } + + /** + * Get user provider object + * + * @access public + * @return UserProviderInterface|LdapUserProvider + */ + public function getUser() + { + return $this->user; + } +} diff --git a/sources/app/Export/SubtaskExport.php b/sources/app/Export/SubtaskExport.php index 386c566..0939838 100644 --- a/sources/app/Export/SubtaskExport.php +++ b/sources/app/Export/SubtaskExport.php @@ -3,9 +3,9 @@ namespace Kanboard\Export; use Kanboard\Core\Base; -use Kanboard\Model\Task; -use Kanboard\Model\Subtask; -use Kanboard\Model\User; +use Kanboard\Model\TaskModel; +use Kanboard\Model\SubtaskModel; +use Kanboard\Model\UserModel; /** * Subtask Export @@ -34,7 +34,7 @@ class SubtaskExport extends Base */ public function export($project_id, $from, $to) { - $this->subtask_status = $this->subtask->getStatusList(); + $this->subtask_status = $this->subtaskModel->getStatusList(); $subtasks = $this->getSubtasks($project_id, $from, $to); $results = array($this->getColumns()); @@ -106,19 +106,19 @@ class SubtaskExport extends Base $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); } - return $this->db->table(Subtask::TABLE) + return $this->db->table(SubtaskModel::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' + SubtaskModel::TABLE.'.*', + UserModel::TABLE.'.username AS assignee_username', + UserModel::TABLE.'.name AS assignee_name', + TaskModel::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') + ->join(TaskModel::TABLE, 'id', 'task_id') + ->join(UserModel::TABLE, 'id', 'user_id') + ->asc(SubtaskModel::TABLE.'.id') ->findAll(); } } diff --git a/sources/app/Export/TaskExport.php b/sources/app/Export/TaskExport.php index b98582a..0e576d3 100644 --- a/sources/app/Export/TaskExport.php +++ b/sources/app/Export/TaskExport.php @@ -4,7 +4,7 @@ namespace Kanboard\Export; use Kanboard\Core\Base; use Kanboard\Core\DateParser; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; use PDO; /** @@ -27,7 +27,7 @@ class TaskExport extends Base public function export($project_id, $from, $to) { $tasks = $this->getTasks($project_id, $from, $to); - $swimlanes = $this->swimlane->getList($project_id); + $swimlanes = $this->swimlaneModel->getList($project_id); $results = array($this->getColumns()); foreach ($tasks as &$task) { @@ -102,9 +102,9 @@ class TaskExport extends Base */ public function format(array &$task, array &$swimlanes) { - $colors = $this->color->getList(); + $colors = $this->colorModel->getList(); - $task['is_active'] = $task['is_active'] == Task::STATUS_OPEN ? e('Open') : e('Closed'); + $task['is_active'] = $task['is_active'] == TaskModel::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']] : '?'; diff --git a/sources/app/Export/TransitionExport.php b/sources/app/Export/TransitionExport.php index 97dc28a..35f9fe4 100644 --- a/sources/app/Export/TransitionExport.php +++ b/sources/app/Export/TransitionExport.php @@ -3,7 +3,6 @@ namespace Kanboard\Export; use Kanboard\Core\Base; -use Kanboard\Core\DateParser; /** * Transition Export @@ -25,7 +24,7 @@ class TransitionExport extends Base public function export($project_id, $from, $to) { $results = array($this->getColumns()); - $transitions = $this->transition->getAllByProjectAndDate($project_id, $from, $to); + $transitions = $this->transitionModel->getAllByProjectAndDate($project_id, $from, $to); foreach ($transitions as $transition) { $results[] = $this->format($transition); @@ -68,7 +67,7 @@ class TransitionExport extends Base $transition['src_column'], $transition['dst_column'], $transition['name'] ?: $transition['username'], - date($this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT), $transition['date']), + date($this->dateParser->getUserDateTimeFormat(), $transition['date']), round($transition['time_spent'] / 3600, 2) ); diff --git a/sources/app/Filter/BaseDateFilter.php b/sources/app/Filter/BaseDateFilter.php new file mode 100644 index 0000000..56fb2d7 --- /dev/null +++ b/sources/app/Filter/BaseDateFilter.php @@ -0,0 +1,103 @@ +dateParser = $dateParser; + return $this; + } + + /** + * Parse operator in the input string + * + * @access protected + * @return string + */ + protected function parseOperator() + { + $operators = array( + '<=' => 'lte', + '>=' => 'gte', + '<' => 'lt', + '>' => 'gt', + ); + + foreach ($operators as $operator => $method) { + if (strpos($this->value, $operator) === 0) { + $this->value = substr($this->value, strlen($operator)); + return $method; + } + } + + return ''; + } + + /** + * Apply a date filter + * + * @access protected + * @param string $field + */ + protected function applyDateFilter($field) + { + $method = $this->parseOperator(); + $timestamp = $this->dateParser->getTimestampFromIsoFormat($this->value); + + if ($method !== '') { + $this->query->$method($field, $this->getTimestampFromOperator($method, $timestamp)); + } else { + $this->query->gte($field, $timestamp); + $this->query->lte($field, $timestamp + 86399); + } + } + + /** + * Get timestamp from the operator + * + * @access public + * @param string $method + * @param integer $timestamp + * @return integer + */ + protected function getTimestampFromOperator($method, $timestamp) + { + switch ($method) { + case 'lte': + return $timestamp + 86399; + case 'lt': + return $timestamp; + case 'gte': + return $timestamp; + case 'gt': + return $timestamp + 86400; + } + + return $timestamp; + } +} diff --git a/sources/app/Filter/BaseFilter.php b/sources/app/Filter/BaseFilter.php new file mode 100644 index 0000000..e029f4e --- /dev/null +++ b/sources/app/Filter/BaseFilter.php @@ -0,0 +1,74 @@ +value = $value; + } + + /** + * Get object instance + * + * @static + * @access public + * @param mixed $value + * @return static + */ + public static function getInstance($value = null) + { + return new static($value); + } + + /** + * Set query + * + * @access public + * @param Table $query + * @return \Kanboard\Core\Filter\FilterInterface + */ + public function withQuery(Table $query) + { + $this->query = $query; + return $this; + } + + /** + * Set the value + * + * @access public + * @param string $value + * @return \Kanboard\Core\Filter\FilterInterface + */ + public function withValue($value) + { + $this->value = $value; + return $this; + } +} diff --git a/sources/app/Filter/ProjectActivityCreationDateFilter.php b/sources/app/Filter/ProjectActivityCreationDateFilter.php new file mode 100644 index 0000000..451f654 --- /dev/null +++ b/sources/app/Filter/ProjectActivityCreationDateFilter.php @@ -0,0 +1,38 @@ +applyDateFilter(ProjectActivityModel::TABLE.'.date_creation'); + return $this; + } +} diff --git a/sources/app/Filter/ProjectActivityCreatorFilter.php b/sources/app/Filter/ProjectActivityCreatorFilter.php new file mode 100644 index 0000000..573238d --- /dev/null +++ b/sources/app/Filter/ProjectActivityCreatorFilter.php @@ -0,0 +1,65 @@ +currentUserId = $userId; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('creator'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + if ($this->value === 'me') { + $this->query->eq(ProjectActivityModel::TABLE . '.creator_id', $this->currentUserId); + } else { + $this->query->beginOr(); + $this->query->ilike('uc.username', '%'.$this->value.'%'); + $this->query->ilike('uc.name', '%'.$this->value.'%'); + $this->query->closeOr(); + } + } +} diff --git a/sources/app/Filter/ProjectActivityProjectIdFilter.php b/sources/app/Filter/ProjectActivityProjectIdFilter.php new file mode 100644 index 0000000..7146a05 --- /dev/null +++ b/sources/app/Filter/ProjectActivityProjectIdFilter.php @@ -0,0 +1,38 @@ +query->eq(ProjectActivityModel::TABLE.'.project_id', $this->value); + return $this; + } +} diff --git a/sources/app/Filter/ProjectActivityProjectIdsFilter.php b/sources/app/Filter/ProjectActivityProjectIdsFilter.php new file mode 100644 index 0000000..70968f7 --- /dev/null +++ b/sources/app/Filter/ProjectActivityProjectIdsFilter.php @@ -0,0 +1,43 @@ +value)) { + $this->query->eq(ProjectActivityModel::TABLE.'.project_id', 0); + } else { + $this->query->in(ProjectActivityModel::TABLE.'.project_id', $this->value); + } + + return $this; + } +} diff --git a/sources/app/Filter/ProjectActivityProjectNameFilter.php b/sources/app/Filter/ProjectActivityProjectNameFilter.php new file mode 100644 index 0000000..b487218 --- /dev/null +++ b/sources/app/Filter/ProjectActivityProjectNameFilter.php @@ -0,0 +1,38 @@ +query->ilike(ProjectModel::TABLE.'.name', '%'.$this->value.'%'); + return $this; + } +} diff --git a/sources/app/Filter/ProjectActivityTaskIdFilter.php b/sources/app/Filter/ProjectActivityTaskIdFilter.php new file mode 100644 index 0000000..b8e074d --- /dev/null +++ b/sources/app/Filter/ProjectActivityTaskIdFilter.php @@ -0,0 +1,38 @@ +query->eq(ProjectActivityModel::TABLE.'.task_id', $this->value); + return $this; + } +} diff --git a/sources/app/Filter/ProjectActivityTaskStatusFilter.php b/sources/app/Filter/ProjectActivityTaskStatusFilter.php new file mode 100644 index 0000000..2c98cab --- /dev/null +++ b/sources/app/Filter/ProjectActivityTaskStatusFilter.php @@ -0,0 +1,43 @@ +value === 'open') { + $this->query->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN); + } elseif ($this->value === 'closed') { + $this->query->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_CLOSED); + } + + return $this; + } +} diff --git a/sources/app/Filter/ProjectActivityTaskTitleFilter.php b/sources/app/Filter/ProjectActivityTaskTitleFilter.php new file mode 100644 index 0000000..bf2afa3 --- /dev/null +++ b/sources/app/Filter/ProjectActivityTaskTitleFilter.php @@ -0,0 +1,25 @@ +query->eq(ProjectGroupRoleModel::TABLE.'.project_id', $this->value); + return $this; + } +} diff --git a/sources/app/Filter/ProjectGroupRoleUsernameFilter.php b/sources/app/Filter/ProjectGroupRoleUsernameFilter.php new file mode 100644 index 0000000..9feac33 --- /dev/null +++ b/sources/app/Filter/ProjectGroupRoleUsernameFilter.php @@ -0,0 +1,44 @@ +query + ->join(GroupMemberModel::TABLE, 'group_id', 'group_id', ProjectGroupRoleModel::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id', GroupMemberModel::TABLE) + ->ilike(UserModel::TABLE.'.username', $this->value.'%'); + + return $this; + } +} diff --git a/sources/app/Filter/ProjectIdsFilter.php b/sources/app/Filter/ProjectIdsFilter.php new file mode 100644 index 0000000..9af9db5 --- /dev/null +++ b/sources/app/Filter/ProjectIdsFilter.php @@ -0,0 +1,43 @@ +value)) { + $this->query->eq(ProjectModel::TABLE.'.id', 0); + } else { + $this->query->in(ProjectModel::TABLE.'.id', $this->value); + } + + return $this; + } +} diff --git a/sources/app/Filter/ProjectStatusFilter.php b/sources/app/Filter/ProjectStatusFilter.php new file mode 100644 index 0000000..c1a06ef --- /dev/null +++ b/sources/app/Filter/ProjectStatusFilter.php @@ -0,0 +1,45 @@ +value) || ctype_digit($this->value)) { + $this->query->eq(ProjectModel::TABLE.'.is_active', $this->value); + } elseif ($this->value === 'inactive' || $this->value === 'closed' || $this->value === 'disabled') { + $this->query->eq(ProjectModel::TABLE.'.is_active', 0); + } else { + $this->query->eq(ProjectModel::TABLE.'.is_active', 1); + } + + return $this; + } +} diff --git a/sources/app/Filter/ProjectTypeFilter.php b/sources/app/Filter/ProjectTypeFilter.php new file mode 100644 index 0000000..6afcd29 --- /dev/null +++ b/sources/app/Filter/ProjectTypeFilter.php @@ -0,0 +1,45 @@ +value) || ctype_digit($this->value)) { + $this->query->eq(ProjectModel::TABLE.'.is_private', $this->value); + } elseif ($this->value === 'private') { + $this->query->eq(ProjectModel::TABLE.'.is_private', ProjectModel::TYPE_PRIVATE); + } else { + $this->query->eq(ProjectModel::TABLE.'.is_private', ProjectModel::TYPE_TEAM); + } + + return $this; + } +} diff --git a/sources/app/Filter/ProjectUserRoleProjectFilter.php b/sources/app/Filter/ProjectUserRoleProjectFilter.php new file mode 100644 index 0000000..6952364 --- /dev/null +++ b/sources/app/Filter/ProjectUserRoleProjectFilter.php @@ -0,0 +1,38 @@ +query->eq(ProjectUserRoleModel::TABLE.'.project_id', $this->value); + return $this; + } +} diff --git a/sources/app/Filter/ProjectUserRoleUsernameFilter.php b/sources/app/Filter/ProjectUserRoleUsernameFilter.php new file mode 100644 index 0000000..327d3d5 --- /dev/null +++ b/sources/app/Filter/ProjectUserRoleUsernameFilter.php @@ -0,0 +1,41 @@ +query + ->join(UserModel::TABLE, 'id', 'user_id') + ->ilike(UserModel::TABLE.'.username', $this->value.'%'); + + return $this; + } +} diff --git a/sources/app/Filter/TaskAssigneeFilter.php b/sources/app/Filter/TaskAssigneeFilter.php new file mode 100644 index 0000000..d6962a9 --- /dev/null +++ b/sources/app/Filter/TaskAssigneeFilter.php @@ -0,0 +1,75 @@ +currentUserId = $userId; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('assignee'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(TaskModel::TABLE.'.owner_id', $this->value); + } else { + switch ($this->value) { + case 'me': + $this->query->eq(TaskModel::TABLE.'.owner_id', $this->currentUserId); + break; + case 'nobody': + $this->query->eq(TaskModel::TABLE.'.owner_id', 0); + break; + default: + $this->query->beginOr(); + $this->query->ilike(UserModel::TABLE.'.username', '%'.$this->value.'%'); + $this->query->ilike(UserModel::TABLE.'.name', '%'.$this->value.'%'); + $this->query->closeOr(); + } + } + } +} diff --git a/sources/app/Filter/TaskCategoryFilter.php b/sources/app/Filter/TaskCategoryFilter.php new file mode 100644 index 0000000..35c52f6 --- /dev/null +++ b/sources/app/Filter/TaskCategoryFilter.php @@ -0,0 +1,46 @@ +value) || ctype_digit($this->value)) { + $this->query->eq(TaskModel::TABLE.'.category_id', $this->value); + } elseif ($this->value === 'none') { + $this->query->eq(TaskModel::TABLE.'.category_id', 0); + } else { + $this->query->eq(CategoryModel::TABLE.'.name', $this->value); + } + + return $this; + } +} diff --git a/sources/app/Filter/TaskColorFilter.php b/sources/app/Filter/TaskColorFilter.php new file mode 100644 index 0000000..2ddb47c --- /dev/null +++ b/sources/app/Filter/TaskColorFilter.php @@ -0,0 +1,60 @@ +colorModel = $colorModel; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('color', 'colour'); + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $this->query->eq(TaskModel::TABLE.'.color_id', $this->colorModel->find($this->value)); + return $this; + } +} diff --git a/sources/app/Filter/TaskColumnFilter.php b/sources/app/Filter/TaskColumnFilter.php new file mode 100644 index 0000000..fa925b7 --- /dev/null +++ b/sources/app/Filter/TaskColumnFilter.php @@ -0,0 +1,44 @@ +value) || ctype_digit($this->value)) { + $this->query->eq(TaskModel::TABLE.'.column_id', $this->value); + } else { + $this->query->eq(ColumnModel::TABLE.'.title', $this->value); + } + + return $this; + } +} diff --git a/sources/app/Filter/TaskCommentFilter.php b/sources/app/Filter/TaskCommentFilter.php new file mode 100644 index 0000000..52db558 --- /dev/null +++ b/sources/app/Filter/TaskCommentFilter.php @@ -0,0 +1,41 @@ +query->ilike(CommentModel::TABLE.'.comment', '%'.$this->value.'%'); + $this->query->join(CommentModel::TABLE, 'task_id', 'id', TaskModel::TABLE); + + return $this; + } +} diff --git a/sources/app/Filter/TaskCompletionDateFilter.php b/sources/app/Filter/TaskCompletionDateFilter.php new file mode 100644 index 0000000..79b5e7d --- /dev/null +++ b/sources/app/Filter/TaskCompletionDateFilter.php @@ -0,0 +1,38 @@ +applyDateFilter(TaskModel::TABLE.'.date_completed'); + return $this; + } +} diff --git a/sources/app/Filter/TaskCreationDateFilter.php b/sources/app/Filter/TaskCreationDateFilter.php new file mode 100644 index 0000000..db28ac8 --- /dev/null +++ b/sources/app/Filter/TaskCreationDateFilter.php @@ -0,0 +1,38 @@ +applyDateFilter(TaskModel::TABLE.'.date_creation'); + return $this; + } +} diff --git a/sources/app/Filter/TaskCreatorFilter.php b/sources/app/Filter/TaskCreatorFilter.php new file mode 100644 index 0000000..611db18 --- /dev/null +++ b/sources/app/Filter/TaskCreatorFilter.php @@ -0,0 +1,74 @@ +currentUserId = $userId; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('creator'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + if (is_int($this->value) || ctype_digit($this->value)) { + $this->query->eq(TaskModel::TABLE.'.creator_id', $this->value); + } else { + switch ($this->value) { + case 'me': + $this->query->eq(TaskModel::TABLE.'.creator_id', $this->currentUserId); + break; + case 'nobody': + $this->query->eq(TaskModel::TABLE.'.creator_id', 0); + break; + default: + $this->query->beginOr(); + $this->query->ilike('uc.username', '%'.$this->value.'%'); + $this->query->ilike('uc.name', '%'.$this->value.'%'); + $this->query->closeOr(); + } + } + } +} diff --git a/sources/app/Filter/TaskDescriptionFilter.php b/sources/app/Filter/TaskDescriptionFilter.php new file mode 100644 index 0000000..c73c2f5 --- /dev/null +++ b/sources/app/Filter/TaskDescriptionFilter.php @@ -0,0 +1,38 @@ +query->ilike(TaskModel::TABLE.'.description', '%'.$this->value.'%'); + return $this; + } +} diff --git a/sources/app/Filter/TaskDueDateFilter.php b/sources/app/Filter/TaskDueDateFilter.php new file mode 100644 index 0000000..0de055b --- /dev/null +++ b/sources/app/Filter/TaskDueDateFilter.php @@ -0,0 +1,41 @@ +query->neq(TaskModel::TABLE.'.date_due', 0); + $this->query->notNull(TaskModel::TABLE.'.date_due'); + $this->applyDateFilter(TaskModel::TABLE.'.date_due'); + + return $this; + } +} diff --git a/sources/app/Filter/TaskDueDateRangeFilter.php b/sources/app/Filter/TaskDueDateRangeFilter.php new file mode 100644 index 0000000..a6aefbe --- /dev/null +++ b/sources/app/Filter/TaskDueDateRangeFilter.php @@ -0,0 +1,39 @@ +query->gte(TaskModel::TABLE.'.date_due', is_numeric($this->value[0]) ? $this->value[0] : strtotime($this->value[0])); + $this->query->lte(TaskModel::TABLE.'.date_due', is_numeric($this->value[1]) ? $this->value[1] : strtotime($this->value[1])); + return $this; + } +} diff --git a/sources/app/Filter/TaskIdExclusionFilter.php b/sources/app/Filter/TaskIdExclusionFilter.php new file mode 100644 index 0000000..20177b2 --- /dev/null +++ b/sources/app/Filter/TaskIdExclusionFilter.php @@ -0,0 +1,38 @@ +query->notin(TaskModel::TABLE.'.id', $this->value); + return $this; + } +} diff --git a/sources/app/Filter/TaskIdFilter.php b/sources/app/Filter/TaskIdFilter.php new file mode 100644 index 0000000..fdf668b --- /dev/null +++ b/sources/app/Filter/TaskIdFilter.php @@ -0,0 +1,38 @@ +query->eq(TaskModel::TABLE.'.id', $this->value); + return $this; + } +} diff --git a/sources/app/Filter/TaskLinkFilter.php b/sources/app/Filter/TaskLinkFilter.php new file mode 100644 index 0000000..98cd597 --- /dev/null +++ b/sources/app/Filter/TaskLinkFilter.php @@ -0,0 +1,85 @@ +db = $db; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('link'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + $task_ids = $this->getSubQuery()->findAllByColumn('task_id'); + + if (! empty($task_ids)) { + $this->query->in(TaskModel::TABLE.'.id', $task_ids); + } else { + $this->query->eq(TaskModel::TABLE.'.id', 0); // No match + } + } + + /** + * Get subquery + * + * @access protected + * @return Table + */ + protected function getSubQuery() + { + return $this->db->table(TaskLinkModel::TABLE) + ->columns( + TaskLinkModel::TABLE.'.task_id', + LinkModel::TABLE.'.label' + ) + ->join(LinkModel::TABLE, 'id', 'link_id', TaskLinkModel::TABLE) + ->ilike(LinkModel::TABLE.'.label', $this->value); + } +} diff --git a/sources/app/Filter/TaskModificationDateFilter.php b/sources/app/Filter/TaskModificationDateFilter.php new file mode 100644 index 0000000..316f183 --- /dev/null +++ b/sources/app/Filter/TaskModificationDateFilter.php @@ -0,0 +1,38 @@ +applyDateFilter(TaskModel::TABLE.'.date_modification'); + return $this; + } +} diff --git a/sources/app/Filter/TaskProjectFilter.php b/sources/app/Filter/TaskProjectFilter.php new file mode 100644 index 0000000..0b5a336 --- /dev/null +++ b/sources/app/Filter/TaskProjectFilter.php @@ -0,0 +1,44 @@ +value) || ctype_digit($this->value)) { + $this->query->eq(TaskModel::TABLE.'.project_id', $this->value); + } else { + $this->query->ilike(ProjectModel::TABLE.'.name', $this->value); + } + + return $this; + } +} diff --git a/sources/app/Filter/TaskProjectsFilter.php b/sources/app/Filter/TaskProjectsFilter.php new file mode 100644 index 0000000..2b6b16c --- /dev/null +++ b/sources/app/Filter/TaskProjectsFilter.php @@ -0,0 +1,43 @@ +value)) { + $this->query->eq(TaskModel::TABLE.'.project_id', 0); + } else { + $this->query->in(TaskModel::TABLE.'.project_id', $this->value); + } + + return $this; + } +} diff --git a/sources/app/Filter/TaskReferenceFilter.php b/sources/app/Filter/TaskReferenceFilter.php new file mode 100644 index 0000000..27c838f --- /dev/null +++ b/sources/app/Filter/TaskReferenceFilter.php @@ -0,0 +1,38 @@ +query->eq(TaskModel::TABLE.'.reference', $this->value); + return $this; + } +} diff --git a/sources/app/Filter/TaskStartDateFilter.php b/sources/app/Filter/TaskStartDateFilter.php new file mode 100644 index 0000000..d5abedb --- /dev/null +++ b/sources/app/Filter/TaskStartDateFilter.php @@ -0,0 +1,38 @@ +applyDateFilter(TaskModel::TABLE.'.date_started'); + return $this; + } +} diff --git a/sources/app/Filter/TaskStatusFilter.php b/sources/app/Filter/TaskStatusFilter.php new file mode 100644 index 0000000..a55532c --- /dev/null +++ b/sources/app/Filter/TaskStatusFilter.php @@ -0,0 +1,43 @@ +value === 'open' || $this->value === 'closed') { + $this->query->eq(TaskModel::TABLE.'.is_active', $this->value === 'open' ? TaskModel::STATUS_OPEN : TaskModel::STATUS_CLOSED); + } else { + $this->query->eq(TaskModel::TABLE.'.is_active', $this->value); + } + + return $this; + } +} diff --git a/sources/app/Filter/TaskSubtaskAssigneeFilter.php b/sources/app/Filter/TaskSubtaskAssigneeFilter.php new file mode 100644 index 0000000..46553a3 --- /dev/null +++ b/sources/app/Filter/TaskSubtaskAssigneeFilter.php @@ -0,0 +1,140 @@ +currentUserId = $userId; + return $this; + } + + /** + * Set database object + * + * @access public + * @param Database $db + * @return TaskSubtaskAssigneeFilter + */ + public function setDatabase(Database $db) + { + $this->db = $db; + return $this; + } + + /** + * Get search attribute + * + * @access public + * @return string[] + */ + public function getAttributes() + { + return array('subtask:assignee'); + } + + /** + * Apply filter + * + * @access public + * @return string + */ + public function apply() + { + $task_ids = $this->getSubQuery()->findAllByColumn('task_id'); + + if (! empty($task_ids)) { + $this->query->in(TaskModel::TABLE.'.id', $task_ids); + } else { + $this->query->eq(TaskModel::TABLE.'.id', 0); // No match + } + } + + /** + * Get subquery + * + * @access protected + * @return Table + */ + protected function getSubQuery() + { + $subquery = $this->db->table(SubtaskModel::TABLE) + ->columns( + SubtaskModel::TABLE.'.user_id', + SubtaskModel::TABLE.'.task_id', + UserModel::TABLE.'.name', + UserModel::TABLE.'.username' + ) + ->join(UserModel::TABLE, 'id', 'user_id', SubtaskModel::TABLE) + ->neq(SubtaskModel::TABLE.'.status', SubtaskModel::STATUS_DONE); + + return $this->applySubQueryFilter($subquery); + } + + /** + * Apply subquery filter + * + * @access protected + * @param Table $subquery + * @return Table + */ + protected function applySubQueryFilter(Table $subquery) + { + if (is_int($this->value) || ctype_digit($this->value)) { + $subquery->eq(SubtaskModel::TABLE.'.user_id', $this->value); + } else { + switch ($this->value) { + case 'me': + $subquery->eq(SubtaskModel::TABLE.'.user_id', $this->currentUserId); + break; + case 'nobody': + $subquery->eq(SubtaskModel::TABLE.'.user_id', 0); + break; + default: + $subquery->beginOr(); + $subquery->ilike(UserModel::TABLE.'.username', $this->value.'%'); + $subquery->ilike(UserModel::TABLE.'.name', '%'.$this->value.'%'); + $subquery->closeOr(); + } + } + + return $subquery; + } +} diff --git a/sources/app/Filter/TaskSwimlaneFilter.php b/sources/app/Filter/TaskSwimlaneFilter.php new file mode 100644 index 0000000..0724396 --- /dev/null +++ b/sources/app/Filter/TaskSwimlaneFilter.php @@ -0,0 +1,50 @@ +value) || ctype_digit($this->value)) { + $this->query->eq(TaskModel::TABLE.'.swimlane_id', $this->value); + } elseif ($this->value === 'default') { + $this->query->eq(TaskModel::TABLE.'.swimlane_id', 0); + } else { + $this->query->beginOr(); + $this->query->ilike(SwimlaneModel::TABLE.'.name', $this->value); + $this->query->ilike(ProjectModel::TABLE.'.default_swimlane', $this->value); + $this->query->closeOr(); + } + + return $this; + } +} diff --git a/sources/app/Filter/TaskTagFilter.php b/sources/app/Filter/TaskTagFilter.php new file mode 100644 index 0000000..01b6f62 --- /dev/null +++ b/sources/app/Filter/TaskTagFilter.php @@ -0,0 +1,74 @@ +db = $db; + return $this; + } + + /** + * Apply filter + * + * @access public + * @return FilterInterface + */ + public function apply() + { + $task_ids = $this->db + ->table(TagModel::TABLE) + ->ilike(TagModel::TABLE.'.name', $this->value) + ->asc(TagModel::TABLE.'.project_id') + ->join(TaskTagModel::TABLE, 'tag_id', 'id') + ->findAllByColumn(TaskTagModel::TABLE.'.task_id'); + + if (empty($task_ids)) { + $task_ids = array(-1); + } + + $this->query->in(TaskModel::TABLE.'.id', $task_ids); + + return $this; + } +} diff --git a/sources/app/Filter/TaskTitleFilter.php b/sources/app/Filter/TaskTitleFilter.php new file mode 100644 index 0000000..4e3a2df --- /dev/null +++ b/sources/app/Filter/TaskTitleFilter.php @@ -0,0 +1,46 @@ +value) || (strlen($this->value) > 1 && $this->value{0} === '#' && ctype_digit(substr($this->value, 1)))) { + $this->query->beginOr(); + $this->query->eq(TaskModel::TABLE.'.id', str_replace('#', '', $this->value)); + $this->query->ilike(TaskModel::TABLE.'.title', '%'.$this->value.'%'); + $this->query->closeOr(); + } else { + $this->query->ilike(TaskModel::TABLE.'.title', '%'.$this->value.'%'); + } + + return $this; + } +} diff --git a/sources/app/Filter/UserNameFilter.php b/sources/app/Filter/UserNameFilter.php new file mode 100644 index 0000000..dfb07fd --- /dev/null +++ b/sources/app/Filter/UserNameFilter.php @@ -0,0 +1,35 @@ +query->beginOr() + ->ilike('username', '%'.$this->value.'%') + ->ilike('name', '%'.$this->value.'%') + ->closeOr(); + + return $this; + } +} diff --git a/sources/app/Formatter/BaseFormatter.php b/sources/app/Formatter/BaseFormatter.php new file mode 100644 index 0000000..89c4843 --- /dev/null +++ b/sources/app/Formatter/BaseFormatter.php @@ -0,0 +1,50 @@ +query = $query; + return $this; + } +} diff --git a/sources/app/Formatter/TaskFilterCalendarEvent.php b/sources/app/Formatter/BaseTaskCalendarFormatter.php similarity index 56% rename from sources/app/Formatter/TaskFilterCalendarEvent.php rename to sources/app/Formatter/BaseTaskCalendarFormatter.php index 12ea868..8fab3e9 100644 --- a/sources/app/Formatter/TaskFilterCalendarEvent.php +++ b/sources/app/Formatter/BaseTaskCalendarFormatter.php @@ -2,7 +2,7 @@ namespace Kanboard\Formatter; -use Kanboard\Model\TaskFilter; +use Kanboard\Core\Filter\FormatterInterface; /** * Common class to handle calendar events @@ -10,7 +10,7 @@ use Kanboard\Model\TaskFilter; * @package formatter * @author Frederic Guillot */ -abstract class TaskFilterCalendarEvent extends TaskFilter +abstract class BaseTaskCalendarFormatter extends BaseFormatter { /** * Column used for event start date @@ -28,21 +28,13 @@ abstract class TaskFilterCalendarEvent extends TaskFilter */ protected $endColumn = 'date_completed'; - /** - * Full day event flag - * - * @access private - * @var boolean - */ - private $fullDay = false; - /** * Transform results to calendar events * * @access public * @param string $start_column Column name for the start date * @param string $end_column Column name for the end date - * @return TaskFilterCalendarEvent + * @return FormatterInterface */ public function setColumns($start_column, $end_column = '') { @@ -50,27 +42,4 @@ abstract class TaskFilterCalendarEvent extends TaskFilter $this->endColumn = $end_column ?: $start_column; return $this; } - - /** - * When called calendar events will be full day - * - * @access public - * @return TaskFilterCalendarEvent - */ - public function setFullDay() - { - $this->fullDay = true; - return $this; - } - - /** - * Return true if the events are full day - * - * @access public - * @return boolean - */ - public function isFullDay() - { - return $this->fullDay; - } } diff --git a/sources/app/Formatter/BoardColumnFormatter.php b/sources/app/Formatter/BoardColumnFormatter.php new file mode 100644 index 0000000..d49a577 --- /dev/null +++ b/sources/app/Formatter/BoardColumnFormatter.php @@ -0,0 +1,94 @@ +swimlaneId = $swimlaneId; + return $this; + } + + /** + * Set columns + * + * @access public + * @param array $columns + * @return $this + */ + public function withColumns(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Set tasks + * + * @access public + * @param array $tasks + * @return $this + */ + public function withTasks(array $tasks) + { + $this->tasks = $tasks; + return $this; + } + + /** + * Set tags + * + * @access public + * @param array $tags + * @return $this + */ + public function withTags(array $tags) + { + $this->tags = $tags; + return $this; + } + + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + foreach ($this->columns as &$column) { + $column['tasks'] = BoardTaskFormatter::getInstance($this->container) + ->withTasks($this->tasks) + ->withTags($this->tags) + ->withSwimlaneId($this->swimlaneId) + ->withColumnId($column['id']) + ->format(); + + $column['nb_tasks'] = count($column['tasks']); + $column['score'] = (int) array_column_sum($column['tasks'], 'score'); + } + + return $this->columns; + } +} diff --git a/sources/app/Formatter/BoardFormatter.php b/sources/app/Formatter/BoardFormatter.php new file mode 100644 index 0000000..350dde6 --- /dev/null +++ b/sources/app/Formatter/BoardFormatter.php @@ -0,0 +1,66 @@ +projectId = $projectId; + return $this; + } + + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + $swimlanes = $this->swimlaneModel->getSwimlanes($this->projectId); + $columns = $this->columnModel->getAll($this->projectId); + $tasks = $this->query + ->eq(TaskModel::TABLE.'.project_id', $this->projectId) + ->asc(TaskModel::TABLE.'.position') + ->findAll(); + + $task_ids = array_column($tasks, 'id'); + $tags = $this->taskTagModel->getTagsByTasks($task_ids); + + if (empty($swimlanes) || empty($columns)) { + return array(); + } + + return BoardSwimlaneFormatter::getInstance($this->container) + ->withSwimlanes($swimlanes) + ->withColumns($columns) + ->withTasks($tasks) + ->withTags($tags) + ->format(); + } +} diff --git a/sources/app/Formatter/BoardSwimlaneFormatter.php b/sources/app/Formatter/BoardSwimlaneFormatter.php new file mode 100644 index 0000000..c2abb44 --- /dev/null +++ b/sources/app/Formatter/BoardSwimlaneFormatter.php @@ -0,0 +1,120 @@ +swimlanes = $swimlanes; + return $this; + } + + /** + * Set columns + * + * @access public + * @param array $columns + * @return $this + */ + public function withColumns($columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Set tasks + * + * @access public + * @param array $tasks + * @return $this + */ + public function withTasks(array $tasks) + { + $this->tasks = $tasks; + return $this; + } + + /** + * Set tags + * + * @access public + * @param array $tags + * @return $this + */ + public function withTags(array $tags) + { + $this->tags = $tags; + return $this; + } + + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + $nb_swimlanes = count($this->swimlanes); + $nb_columns = count($this->columns); + + foreach ($this->swimlanes as &$swimlane) { + $swimlane['columns'] = BoardColumnFormatter::getInstance($this->container) + ->withSwimlaneId($swimlane['id']) + ->withColumns($this->columns) + ->withTasks($this->tasks) + ->withTags($this->tags) + ->format(); + + $swimlane['nb_swimlanes'] = $nb_swimlanes; + $swimlane['nb_columns'] = $nb_columns; + $swimlane['nb_tasks'] = array_column_sum($swimlane['columns'], 'nb_tasks'); + $swimlane['score'] = array_column_sum($swimlane['columns'], 'score'); + + $this->calculateStatsByColumnAcrossSwimlanes($swimlane['columns']); + } + + return $this->swimlanes; + } + + /** + * Calculate stats for each column acrosss all swimlanes + * + * @access protected + * @param array $columns + */ + protected function calculateStatsByColumnAcrossSwimlanes(array $columns) + { + foreach ($columns as $columnIndex => $column) { + if (! isset($this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks'])) { + $this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks'] = 0; + $this->swimlanes[0]['columns'][$columnIndex]['column_score'] = 0; + } + + $this->swimlanes[0]['columns'][$columnIndex]['column_nb_tasks'] += $column['nb_tasks']; + $this->swimlanes[0]['columns'][$columnIndex]['column_score'] += $column['score']; + } + } +} diff --git a/sources/app/Formatter/BoardTaskFormatter.php b/sources/app/Formatter/BoardTaskFormatter.php new file mode 100644 index 0000000..3bf171b --- /dev/null +++ b/sources/app/Formatter/BoardTaskFormatter.php @@ -0,0 +1,96 @@ +tags = $tags; + return $this; + } + + /** + * Set tasks + * + * @access public + * @param array $tasks + * @return $this + */ + public function withTasks(array $tasks) + { + $this->tasks = $tasks; + return $this; + } + + /** + * Set columnId + * + * @access public + * @param integer $columnId + * @return $this + */ + public function withColumnId($columnId) + { + $this->columnId = $columnId; + return $this; + } + + /** + * Set swimlaneId + * + * @access public + * @param integer $swimlaneId + * @return $this + */ + public function withSwimlaneId($swimlaneId) + { + $this->swimlaneId = $swimlaneId; + return $this; + } + + /** + * Apply formatter + * + * @access public + * @return array + */ + public function format() + { + $tasks = array_values(array_filter($this->tasks, array($this, 'filterTasks'))); + array_merge_relation($tasks, $this->tags, 'tags', 'id'); + return $tasks; + } + + /** + * Keep only tasks of the given column and swimlane + * + * @access protected + * @param array $task + * @return bool + */ + protected function filterTasks(array $task) + { + return $task['column_id'] == $this->columnId && $task['swimlane_id'] == $this->swimlaneId; + } +} diff --git a/sources/app/Formatter/FormatterInterface.php b/sources/app/Formatter/FormatterInterface.php deleted file mode 100644 index 0bb6129..0000000 --- a/sources/app/Formatter/FormatterInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -groups = $groups; + } + + /** + * Set query + * + * @access public + * @param Table $query + * @return FormatterInterface + */ + public function withQuery(Table $query) + { return $this; } /** - * Format groups for the ajax autocompletion + * Format groups for the ajax auto-completion * * @access public * @return array diff --git a/sources/app/Formatter/ProjectActivityEventFormatter.php b/sources/app/Formatter/ProjectActivityEventFormatter.php new file mode 100644 index 0000000..aa0ea7c --- /dev/null +++ b/sources/app/Formatter/ProjectActivityEventFormatter.php @@ -0,0 +1,61 @@ +query->findAll(); + + foreach ($events as &$event) { + $event += $this->unserializeEvent($event['data']); + unset($event['data']); + + $event['author'] = $event['author_name'] ?: $event['author_username']; + $event['event_title'] = $this->notificationModel->getTitleWithAuthor($event['author'], $event['event_name'], $event); + $event['event_content'] = $this->renderEvent($event); + } + + return $events; + } + + /** + * Decode event data, supports unserialize() and json_decode() + * + * @access protected + * @param string $data Serialized data + * @return array + */ + protected function unserializeEvent($data) + { + if ($data{0} === 'a') { + return unserialize($data); + } + + return json_decode($data, true) ?: array(); + } + + /** + * Get the event html content + * + * @access protected + * @param array $params Event properties + * @return string + */ + protected function renderEvent(array $params) + { + return $this->template->render( + 'event/'.str_replace('.', '_', $params['event_name']), + $params + ); + } +} diff --git a/sources/app/Formatter/ProjectGanttFormatter.php b/sources/app/Formatter/ProjectGanttFormatter.php index 4f73e21..af04f49 100644 --- a/sources/app/Formatter/ProjectGanttFormatter.php +++ b/sources/app/Formatter/ProjectGanttFormatter.php @@ -2,7 +2,7 @@ namespace Kanboard\Formatter; -use Kanboard\Model\Project; +use Kanboard\Core\Filter\FormatterInterface; /** * Gantt chart formatter for projects @@ -10,40 +10,8 @@ use Kanboard\Model\Project; * @package formatter * @author Frederic Guillot */ -class ProjectGanttFormatter extends Project implements FormatterInterface +class ProjectGanttFormatter extends BaseFormatter implements FormatterInterface { - /** - * List of projects - * - * @access private - * @var array - */ - private $projects = array(); - - /** - * Filter projects to generate the Gantt chart - * - * @access public - * @param int[] $project_ids - * @return ProjectGanttFormatter - */ - public function filter(array $project_ids) - { - if (empty($project_ids)) { - $this->projects = array(); - } else { - $this->projects = $this->db - ->table(self::TABLE) - ->asc('start_date') - ->in('id', $project_ids) - ->eq('is_active', self::ACTIVE) - ->eq('is_private', 0) - ->findAll(); - } - - return $this; - } - /** * Format projects to be displayed in the Gantt chart * @@ -52,10 +20,11 @@ class ProjectGanttFormatter extends Project implements FormatterInterface */ public function format() { - $colors = $this->color->getDefaultColors(); + $projects = $this->query->findAll(); + $colors = $this->colorModel->getDefaultColors(); $bars = array(); - foreach ($this->projects as $project) { + foreach ($projects as $project) { $start = empty($project['start_date']) ? time() : strtotime($project['start_date']); $end = empty($project['end_date']) ? $start : strtotime($project['end_date']); $color = next($colors) ?: reset($colors); @@ -74,12 +43,12 @@ class ProjectGanttFormatter extends Project implements FormatterInterface (int) date('n', $end), (int) date('j', $end), ), - 'link' => $this->helper->url->href('project', 'show', array('project_id' => $project['id'])), - 'board_link' => $this->helper->url->href('board', 'show', array('project_id' => $project['id'])), - 'gantt_link' => $this->helper->url->href('gantt', 'project', array('project_id' => $project['id'])), + 'link' => $this->helper->url->href('ProjectViewController', 'show', array('project_id' => $project['id'])), + 'board_link' => $this->helper->url->href('BoardViewController', 'show', array('project_id' => $project['id'])), + 'gantt_link' => $this->helper->url->href('TaskGanttController', 'show', array('project_id' => $project['id'])), 'color' => $color, 'not_defined' => empty($project['start_date']) || empty($project['end_date']), - 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']), + 'users' => $this->projectUserRoleModel->getAllUsersGroupedByRole($project['id']), ); } diff --git a/sources/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php b/sources/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php new file mode 100644 index 0000000..b7b81d8 --- /dev/null +++ b/sources/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php @@ -0,0 +1,38 @@ +query->findAll() as $row) { + $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : ''; + + $events[] = array( + 'id' => $row['id'], + 'subtask_id' => $row['subtask_id'], + 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user, + 'start' => date('Y-m-d\TH:i:s', $row['start']), + 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()), + 'backgroundColor' => $this->colorModel->getBackgroundColor($row['color_id']), + 'borderColor' => $this->colorModel->getBorderColor($row['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url->to('TaskViewController', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])), + 'editable' => false, + ); + } + + return $events; + } +} diff --git a/sources/app/Formatter/TaskAutoCompleteFormatter.php b/sources/app/Formatter/TaskAutoCompleteFormatter.php new file mode 100644 index 0000000..4f1c4c6 --- /dev/null +++ b/sources/app/Formatter/TaskAutoCompleteFormatter.php @@ -0,0 +1,33 @@ +query->columns(TaskModel::TABLE.'.id', TaskModel::TABLE.'.title')->findAll(); + + foreach ($tasks as &$task) { + $task['value'] = $task['title']; + $task['label'] = '#'.$task['id'].' - '.$task['title']; + } + + return $tasks; + } +} diff --git a/sources/app/Formatter/TaskCalendarFormatter.php b/sources/app/Formatter/TaskCalendarFormatter.php new file mode 100644 index 0000000..75d2a83 --- /dev/null +++ b/sources/app/Formatter/TaskCalendarFormatter.php @@ -0,0 +1,74 @@ +fullDay = true; + return $this; + } + + /** + * Transform tasks to calendar events + * + * @access public + * @return array + */ + public function format() + { + $events = array(); + + foreach ($this->query->findAll() as $task) { + $events[] = array( + 'timezoneParam' => $this->timezoneModel->getCurrentTimezone(), + 'id' => $task['id'], + 'title' => t('#%d', $task['id']).' '.$task['title'], + 'backgroundColor' => $this->colorModel->getBackgroundColor($task['color_id']), + 'borderColor' => $this->colorModel->getBorderColor($task['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + 'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]), + 'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()), + 'editable' => $this->fullDay, + 'allday' => $this->fullDay, + ); + } + + return $events; + } + + /** + * Get DateTime format for event + * + * @access private + * @return string + */ + private function getDateTimeFormat() + { + return $this->fullDay ? 'Y-m-d' : 'Y-m-d\TH:i:s'; + } +} diff --git a/sources/app/Formatter/TaskFilterAutoCompleteFormatter.php b/sources/app/Formatter/TaskFilterAutoCompleteFormatter.php deleted file mode 100644 index c9af465..0000000 --- a/sources/app/Formatter/TaskFilterAutoCompleteFormatter.php +++ /dev/null @@ -1,33 +0,0 @@ -query->columns(Task::TABLE.'.id', Task::TABLE.'.title')->findAll(); - - foreach ($tasks as &$task) { - $task['value'] = $task['title']; - $task['label'] = '#'.$task['id'].' - '.$task['title']; - } - - return $tasks; - } -} diff --git a/sources/app/Formatter/TaskFilterCalendarFormatter.php b/sources/app/Formatter/TaskFilterCalendarFormatter.php deleted file mode 100644 index 1b5d6ca..0000000 --- a/sources/app/Formatter/TaskFilterCalendarFormatter.php +++ /dev/null @@ -1,52 +0,0 @@ -query->findAll() as $task) { - $events[] = 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'])), - 'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]), - 'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()), - 'editable' => $this->isFullDay(), - 'allday' => $this->isFullDay(), - ); - } - - return $events; - } - - /** - * Get DateTime format for event - * - * @access private - * @return string - */ - private function getDateTimeFormat() - { - return $this->isFullDay() ? 'Y-m-d' : 'Y-m-d\TH:i:s'; - } -} diff --git a/sources/app/Formatter/TaskFilterGanttFormatter.php b/sources/app/Formatter/TaskGanttFormatter.php similarity index 69% rename from sources/app/Formatter/TaskFilterGanttFormatter.php rename to sources/app/Formatter/TaskGanttFormatter.php index a4eef1e..ddb3f93 100644 --- a/sources/app/Formatter/TaskFilterGanttFormatter.php +++ b/sources/app/Formatter/TaskGanttFormatter.php @@ -2,15 +2,15 @@ namespace Kanboard\Formatter; -use Kanboard\Model\TaskFilter; +use Kanboard\Core\Filter\FormatterInterface; /** - * Gantt chart formatter for task filter + * Task Gantt Formatter * - * @package formatter - * @author Frederic Guillot + * @package formatter + * @author Frederic Guillot */ -class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface +class TaskGanttFormatter extends BaseFormatter implements FormatterInterface { /** * Local cache for project columns @@ -19,9 +19,9 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface * @var array */ private $columns = array(); - + /** - * Format tasks to be displayed in the Gantt chart + * Apply formatter * * @access public * @return array @@ -47,7 +47,7 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface private function formatTask(array $task) { if (! isset($this->columns[$task['project_id']])) { - $this->columns[$task['project_id']] = $this->column->getList($task['project_id']); + $this->columns[$task['project_id']] = $this->columnModel->getList($task['project_id']); } $start = $task['date_started'] ?: time(); @@ -69,9 +69,9 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface ), 'column_title' => $task['column_name'], 'assignee' => $task['assignee_name'] ?: $task['assignee_username'], - 'progress' => $this->task->getProgress($task, $this->columns[$task['project_id']]).'%', - 'link' => $this->helper->url->href('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), - 'color' => $this->color->getColorProperties($task['color_id']), + 'progress' => $this->taskModel->getProgress($task, $this->columns[$task['project_id']]).'%', + 'link' => $this->helper->url->href('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), + 'color' => $this->colorModel->getColorProperties($task['color_id']), 'not_defined' => empty($task['date_due']) || empty($task['date_started']), ); } diff --git a/sources/app/Formatter/TaskFilterICalendarFormatter.php b/sources/app/Formatter/TaskICalFormatter.php similarity index 71% rename from sources/app/Formatter/TaskFilterICalendarFormatter.php rename to sources/app/Formatter/TaskICalFormatter.php index 25b3aea..ad2a444 100644 --- a/sources/app/Formatter/TaskFilterICalendarFormatter.php +++ b/sources/app/Formatter/TaskICalFormatter.php @@ -6,14 +6,16 @@ use DateTime; use Eluceo\iCal\Component\Calendar; use Eluceo\iCal\Component\Event; use Eluceo\iCal\Property\Event\Attendees; +use Eluceo\iCal\Property\Event\Organizer; +use Kanboard\Core\Filter\FormatterInterface; /** - * iCal event formatter for task filter + * iCal event formatter for tasks * * @package formatter * @author Frederic Guillot */ -class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface +class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterInterface { /** * Calendar object @@ -39,7 +41,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo * * @access public * @param \Eluceo\iCal\Component\Calendar $vCalendar - * @return TaskFilterICalendarFormatter + * @return FormatterInterface */ public function setCalendar(Calendar $vCalendar) { @@ -48,10 +50,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo } /** - * Transform results to ical events + * Transform results to iCal events * * @access public - * @return TaskFilterICalendarFormatter + * @return FormatterInterface */ public function addDateTimeEvents() { @@ -73,10 +75,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo } /** - * Transform results to all day ical events + * Transform results to all day iCal events * * @access public - * @return TaskFilterICalendarFormatter + * @return FormatterInterface */ public function addFullDayEvents() { @@ -96,7 +98,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo } /** - * Get common events for task ical events + * Get common events for task iCal events * * @access protected * @param array $task @@ -116,16 +118,24 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo $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']))); + $vEvent->setDescription($task['description']); + $vEvent->setDescriptionHTML($this->helper->text->markdown($task['description'])); + $vEvent->setUrl($this->helper->url->base().$this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); if (! empty($task['owner_id'])) { - $vEvent->setOrganizer($task['assignee_name'] ?: $task['assignee_username'], $task['assignee_email']); + $attendees = new Attendees; + $attendees->add( + 'MAILTO:'.($task['assignee_email'] ?: $task['assignee_username'].'@kanboard.local'), + array('CN' => $task['assignee_name'] ?: $task['assignee_username']) + ); + $vEvent->setAttendees($attendees); } if (! empty($task['creator_id'])) { - $attendees = new Attendees; - $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); - $vEvent->setAttendees($attendees); + $vEvent->setOrganizer(new Organizer( + 'MAILTO:' . $task['creator_email'] ?: $task['creator_username'].'@kanboard.local', + array('CN' => $task['creator_name'] ?: $task['creator_username']) + )); } return $vEvent; diff --git a/sources/app/Formatter/UserFilterAutoCompleteFormatter.php b/sources/app/Formatter/UserAutoCompleteFormatter.php similarity index 64% rename from sources/app/Formatter/UserFilterAutoCompleteFormatter.php rename to sources/app/Formatter/UserAutoCompleteFormatter.php index b98e0d6..cd23a2a 100644 --- a/sources/app/Formatter/UserFilterAutoCompleteFormatter.php +++ b/sources/app/Formatter/UserAutoCompleteFormatter.php @@ -2,16 +2,16 @@ namespace Kanboard\Formatter; -use Kanboard\Model\User; -use Kanboard\Model\UserFilter; +use Kanboard\Model\UserModel; +use Kanboard\Core\Filter\FormatterInterface; /** - * Autocomplete formatter for user filter + * Auto-complete formatter for user filter * * @package formatter * @author Frederic Guillot */ -class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInterface +class UserAutoCompleteFormatter extends BaseFormatter implements FormatterInterface { /** * Format the tasks for the ajax autocompletion @@ -21,7 +21,7 @@ class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInt */ public function format() { - $users = $this->query->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')->findAll(); + $users = $this->query->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name')->findAll(); foreach ($users as &$user) { $user['value'] = $user['username'].' (#'.$user['id'].')'; diff --git a/sources/app/Group/DatabaseBackendGroupProvider.php b/sources/app/Group/DatabaseBackendGroupProvider.php index 6dbaa43..29d04d5 100644 --- a/sources/app/Group/DatabaseBackendGroupProvider.php +++ b/sources/app/Group/DatabaseBackendGroupProvider.php @@ -23,7 +23,7 @@ class DatabaseBackendGroupProvider extends Base implements GroupBackendProviderI public function find($input) { $result = array(); - $groups = $this->group->search($input); + $groups = $this->groupModel->search($input); foreach ($groups as $group) { $result[] = new DatabaseGroupProvider($group); diff --git a/sources/app/Helper/AppHelper.php b/sources/app/Helper/AppHelper.php index e6f6412..09f280c 100644 --- a/sources/app/Helper/AppHelper.php +++ b/sources/app/Helper/AppHelper.php @@ -22,7 +22,7 @@ class AppHelper extends Base */ public function config($param, $default_value = '') { - return $this->config->get($param, $default_value); + return $this->configModel->get($param, $default_value); } /** @@ -90,7 +90,40 @@ class AppHelper extends Base */ public function jsLang() { - return $this->config->getJsLanguageCode(); + return $this->languageModel->getJsLanguageCode(); + } + + /** + * Get date format for Jquery DatePicker + * + * @access public + * @return string + */ + public function getJsDateFormat() + { + $format = $this->dateParser->getUserDateFormat(); + $format = str_replace('m', 'mm', $format); + $format = str_replace('Y', 'yy', $format); + $format = str_replace('d', 'dd', $format); + + return $format; + } + + /** + * Get time format for Jquery Plugin DateTimePicker + * + * @access public + * @return string + */ + public function getJsTimeFormat() + { + $format = $this->dateParser->getUserTimeFormat(); + $format = str_replace('H', 'HH', $format); + $format = str_replace('i', 'mm', $format); + $format = str_replace('g', 'h', $format); + $format = str_replace('a', 'tt', $format); + + return $format; } /** @@ -101,7 +134,7 @@ class AppHelper extends Base */ public function getTimezone() { - return $this->config->getCurrentTimezone(); + return $this->timezoneModel->getCurrentTimezone(); } /** diff --git a/sources/app/Helper/AssetHelper.php b/sources/app/Helper/AssetHelper.php index b3dc711..dad1448 100644 --- a/sources/app/Helper/AssetHelper.php +++ b/sources/app/Helper/AssetHelper.php @@ -45,8 +45,8 @@ class AssetHelper extends Base */ public function customCss() { - if ($this->config->get('application_stylesheet')) { - return ''; + if ($this->configModel->get('application_stylesheet')) { + return ''; } return ''; @@ -60,6 +60,6 @@ class AssetHelper extends Base */ public function colorCss() { - return ''; + return ''; } } diff --git a/sources/app/Helper/CalendarHelper.php b/sources/app/Helper/CalendarHelper.php new file mode 100644 index 0000000..b35c40f --- /dev/null +++ b/sources/app/Helper/CalendarHelper.php @@ -0,0 +1,112 @@ +container); + $formatter->setFullDay(); + $formatter->setColumns('date_due'); + + return $queryBuilder + ->withFilter(new TaskDueDateRangeFilter(array($start, $end))) + ->format($formatter); + } + + /** + * Get formatted calendar task events + * + * @access public + * @param QueryBuilder $queryBuilder + * @param string $start + * @param string $end + * @return array + */ + public function getTaskEvents(QueryBuilder $queryBuilder, $start, $end) + { + $startColumn = $this->configModel->get('calendar_project_tasks', 'date_started'); + + $queryBuilder->getQuery()->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + $startColumn, + 'date_due' + )); + + $formatter = new TaskCalendarFormatter($this->container); + $formatter->setColumns($startColumn, 'date_due'); + + return $queryBuilder->format($formatter); + } + + /** + * Get formatted calendar subtask time tracking events + * + * @access public + * @param integer $user_id + * @param string $start + * @param string $end + * @return array + */ + public function getSubtaskTimeTrackingEvents($user_id, $start, $end) + { + $formatter = new SubtaskTimeTrackingCalendarFormatter($this->container); + return $formatter + ->withQuery($this->subtaskTimeTrackingModel->getUserQuery($user_id) + ->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + 'start', + 'end' + )) + ) + ->format(); + } + + /** + * Build SQL condition for a given time range + * + * @access public + * @param string $start_time Start timestamp + * @param string $end_time End timestamp + * @param string $start_column Start column name + * @param string $end_column End column name + * @return string + */ + public function getCalendarCondition($start_time, $end_time, $start_column, $end_column) + { + $start_column = $this->db->escapeIdentifier($start_column); + $end_column = $this->db->escapeIdentifier($end_column); + + $conditions = array( + "($start_column >= '$start_time' AND $start_column <= '$end_time')", + "($start_column <= '$start_time' AND $end_column >= '$start_time')", + "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))", + ); + + return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')'; + } +} diff --git a/sources/app/Helper/DateHelper.php b/sources/app/Helper/DateHelper.php index 3844ce6..7e2ec79 100644 --- a/sources/app/Helper/DateHelper.php +++ b/sources/app/Helper/DateHelper.php @@ -22,7 +22,7 @@ class DateHelper extends Base */ public function time($value) { - return date($this->config->get('application_time_format', 'H:i'), $value); + return date($this->configModel->get('application_time_format', 'H:i'), $value); } /** @@ -42,7 +42,7 @@ class DateHelper extends Base $value = strtotime($value); } - return date($this->config->get('application_date_format', 'm/d/Y'), $value); + return date($this->configModel->get('application_date_format', 'm/d/Y'), $value); } /** @@ -54,7 +54,7 @@ class DateHelper extends Base */ public function datetime($value) { - return date($this->config->get('application_datetime_format', 'm/d/Y H:i'), $value); + return date($this->configModel->get('application_datetime_format', 'm/d/Y H:i'), $value); } /** diff --git a/sources/app/Helper/ICalHelper.php b/sources/app/Helper/ICalHelper.php new file mode 100644 index 0000000..dc399bf --- /dev/null +++ b/sources/app/Helper/ICalHelper.php @@ -0,0 +1,38 @@ +withFilter(new TaskDueDateRangeFilter(array($start, $end))); + + $formatter = new TaskICalFormatter($this->container); + $formatter->setColumns('date_due'); + $formatter->setCalendar($calendar); + $formatter->withQuery($queryBuilder->getQuery()); + $formatter->addFullDayEvents(); + } +} diff --git a/sources/app/Helper/LayoutHelper.php b/sources/app/Helper/LayoutHelper.php index 9384da1..8ebb05d 100644 --- a/sources/app/Helper/LayoutHelper.php +++ b/sources/app/Helper/LayoutHelper.php @@ -27,7 +27,7 @@ class LayoutHelper extends Base } if (! isset($params['no_layout']) && ! isset($params['board_selector'])) { - $params['board_selector'] = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId()); + $params['board_selector'] = $this->projectUserRoleModel->getActiveProjectsByUser($this->userSession->getId()); } return $this->pageLayout($template, $params); @@ -47,7 +47,7 @@ class LayoutHelper extends Base $params['title'] = '#'.$params['user']['id'].' '.($params['user']['name'] ?: $params['user']['username']); } - return $this->subLayout('user/layout', 'user/sidebar', $template, $params); + return $this->subLayout('user_view/layout', 'user_view/sidebar', $template, $params); } /** @@ -60,6 +60,7 @@ class LayoutHelper extends Base */ public function task($template, array $params) { + $params['page_title'] = $params['task']['project_name'].', #'.$params['task']['id'].' - '.$params['task']['title']; $params['title'] = $params['task']['project_name']; return $this->subLayout('task/layout', 'task/sidebar', $template, $params); } @@ -95,7 +96,7 @@ class LayoutHelper extends Base public function projectUser($template, array $params) { $params['filter'] = array('user_id' => $params['user_id']); - return $this->subLayout('project_user/layout', 'project_user/sidebar', $template, $params); + return $this->subLayout('project_user_overview/layout', 'project_user_overview/sidebar', $template, $params); } /** @@ -109,7 +110,7 @@ class LayoutHelper extends Base public function config($template, array $params) { if (! isset($params['values'])) { - $params['values'] = $this->config->getAll(); + $params['values'] = $this->configModel->getAll(); } if (! isset($params['errors'])) { @@ -119,6 +120,19 @@ class LayoutHelper extends Base return $this->subLayout('config/layout', 'config/sidebar', $template, $params); } + /** + * Common layout for plugin views + * + * @access public + * @param string $template + * @param array $params + * @return string + */ + public function plugin($template, array $params) + { + return $this->subLayout('plugin/layout', 'plugin/sidebar', $template, $params); + } + /** * Common layout for dashboard views * @@ -129,7 +143,7 @@ class LayoutHelper extends Base */ public function dashboard($template, array $params) { - return $this->subLayout('app/layout', 'app/sidebar', $template, $params); + return $this->subLayout('dashboard/layout', 'dashboard/sidebar', $template, $params); } /** diff --git a/sources/app/Helper/MailHelper.php b/sources/app/Helper/MailHelper.php new file mode 100644 index 0000000..3b1c9e4 --- /dev/null +++ b/sources/app/Helper/MailHelper.php @@ -0,0 +1,82 @@ +configModel->get('mail_sender_address'); + + if (!empty($email)) { + return $email; + } + + return MAIL_FROM; + } + + /** + * Get mail sender address + * + * @access public + * @return string + */ + public function getMailTransport() + { + $transport = $this->configModel->get('mail_transport'); + + if (!empty($transport)) { + return $transport; + } + + return MAIL_TRANSPORT; + } +} diff --git a/sources/app/Helper/ProjectActivityHelper.php b/sources/app/Helper/ProjectActivityHelper.php new file mode 100644 index 0000000..40f386d --- /dev/null +++ b/sources/app/Helper/ProjectActivityHelper.php @@ -0,0 +1,105 @@ +projectUserRoleModel->getProjectsByUser($this->userSession->getId()); + $events = array(); + + if ($search !== '') { + $queryBuilder = $this->projectActivityLexer->build($search); + $queryBuilder + ->withFilter(new ProjectActivityProjectIdsFilter(array_keys($projects))) + ->getQuery() + ->desc(ProjectActivityModel::TABLE.'.id') + ->limit(500) + ; + + $events = $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } + + return $events; + } + + /** + * Get project activity events + * + * @access public + * @param integer $project_id + * @param int $limit + * @return array + */ + public function getProjectEvents($project_id, $limit = 50) + { + $queryBuilder = $this->projectActivityQuery + ->withFilter(new ProjectActivityProjectIdFilter($project_id)); + + $queryBuilder->getQuery() + ->desc(ProjectActivityModel::TABLE.'.id') + ->limit($limit) + ; + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } + + /** + * Get projects activity events + * + * @access public + * @param int[] $project_ids + * @param int $limit + * @return array + */ + public function getProjectsEvents(array $project_ids, $limit = 50) + { + $queryBuilder = $this->projectActivityQuery + ->withFilter(new ProjectActivityProjectIdsFilter($project_ids)); + + $queryBuilder->getQuery() + ->desc(ProjectActivityModel::TABLE.'.id') + ->limit($limit) + ; + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } + + /** + * Get task activity events + * + * @access public + * @param integer $task_id + * @return array + */ + public function getTaskEvents($task_id) + { + $queryBuilder = $this->projectActivityQuery + ->withFilter(new ProjectActivityTaskIdFilter($task_id)); + + $queryBuilder->getQuery()->desc(ProjectActivityModel::TABLE.'.id'); + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } +} diff --git a/sources/app/Helper/ProjectHeaderHelper.php b/sources/app/Helper/ProjectHeaderHelper.php index 1957005..9514f4f 100644 --- a/sources/app/Helper/ProjectHeaderHelper.php +++ b/sources/app/Helper/ProjectHeaderHelper.php @@ -48,9 +48,9 @@ class ProjectHeaderHelper extends Base return $this->template->render('project_header/header', array( 'project' => $project, 'filters' => $filters, - 'categories_list' => $this->category->getList($project['id'], false), - 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], false), - 'custom_filters_list' => $this->customFilter->getAll($project['id'], $this->userSession->getId()), + 'categories_list' => $this->categoryModel->getList($project['id'], false), + 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id'], false), + 'custom_filters_list' => $this->customFilterModel->getAll($project['id'], $this->userSession->getId()), 'board_view' => $boardView, )); } diff --git a/sources/app/Helper/SubtaskHelper.php b/sources/app/Helper/SubtaskHelper.php index afa3c14..dac7120 100644 --- a/sources/app/Helper/SubtaskHelper.php +++ b/sources/app/Helper/SubtaskHelper.php @@ -36,18 +36,18 @@ class SubtaskHelper extends Base */ public function toggleStatus(array $subtask, $project_id, $refresh_table = false) { - if (! $this->helper->user->hasProjectAccess('subtask', 'edit', $project_id)) { + if (! $this->helper->user->hasProjectAccess('SubtaskController', 'edit', $project_id)) { return $this->getTitle($subtask); } $params = array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'refresh-table' => (int) $refresh_table); if ($subtask['status'] == 0 && isset($this->sessionStorage->hasSubtaskInProgress) && $this->sessionStorage->hasSubtaskInProgress) { - return $this->helper->url->link($this->getTitle($subtask), 'SubtaskRestriction', 'popover', $params, false, 'popover'); + return $this->helper->url->link($this->getTitle($subtask), 'SubtaskRestrictionController', 'show', $params, false, 'popover'); } $class = 'subtask-toggle-status '.($refresh_table ? 'subtask-refresh-table' : ''); - return $this->helper->url->link($this->getTitle($subtask), 'SubtaskStatus', 'change', $params, false, $class); + return $this->helper->url->link($this->getTitle($subtask), 'SubtaskStatusController', 'change', $params, false, $class); } public function selectTitle(array $values, array $errors = array(), array $attributes = array()) diff --git a/sources/app/Helper/TaskHelper.php b/sources/app/Helper/TaskHelper.php index 4857d0e..e1d65cc 100644 --- a/sources/app/Helper/TaskHelper.php +++ b/sources/app/Helper/TaskHelper.php @@ -22,27 +22,77 @@ class TaskHelper extends Base public function getColors() { - return $this->color->getList(); + return $this->colorModel->getList(); } public function recurrenceTriggers() { - return $this->task->getRecurrenceTriggerList(); + return $this->taskRecurrenceModel->getRecurrenceTriggerList(); } public function recurrenceTimeframes() { - return $this->task->getRecurrenceTimeframeList(); + return $this->taskRecurrenceModel->getRecurrenceTimeframeList(); } public function recurrenceBasedates() { - return $this->task->getRecurrenceBasedateList(); + return $this->taskRecurrenceModel->getRecurrenceBasedateList(); } - public function canRemove(array $task) + public function selectTitle(array $values, array $errors) { - return $this->taskPermission->canRemoveTask($task); + $html = $this->helper->form->label(t('Title'), 'title'); + $html .= $this->helper->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large'); + return $html; + } + + public function selectDescription(array $values, array $errors) + { + $html = $this->helper->form->label(t('Description'), 'description'); + $html .= $this->helper->form->textarea( + 'description', + $values, + $errors, + array( + 'placeholder="'.t('Leave a description').'"', + 'tabindex="2"', + 'data-mention-search-url="'.$this->helper->url->href('UserAjaxController', 'mention', array('project_id' => $values['project_id'])).'"' + ), + 'markdown-editor' + ); + + return $html; + } + + public function selectTags(array $project, array $tags = array()) + { + $options = $this->tagModel->getAssignableList($project['id']); + + $html = $this->helper->form->label(t('Tags'), 'tags[]'); + $html .= ''; + $html .= ''; + + return $html; + } + + public function selectColor(array $values) + { + $colors = $this->colorModel->getList(); + $html = $this->helper->form->label(t('Color'), 'color_id'); + $html .= $this->helper->form->select('color_id', $colors, $values, array(), array(), 'color-picker'); + return $html; } public function selectAssignee(array $users, array $values, array $errors = array(), array $attributes = array()) @@ -118,10 +168,20 @@ class TaskHelper extends Base return $html; } - public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array()) + public function selectReference(array $values, array $errors = array(), array $attributes = array()) { $attributes = array_merge(array('tabindex="9"'), $attributes); + $html = $this->helper->form->label(t('Reference'), 'reference'); + $html .= $this->helper->form->text('reference', $values, $errors, $attributes, 'form-input-small'); + + return $html; + } + + public function selectTimeEstimated(array $values, array $errors = array(), array $attributes = array()) + { + $attributes = array_merge(array('tabindex="10"'), $attributes); + $html = $this->helper->form->label(t('Original estimate'), 'time_estimated'); $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes); $html .= ' '.t('hours'); @@ -131,7 +191,7 @@ class TaskHelper extends Base public function selectTimeSpent(array $values, array $errors = array(), array $attributes = array()) { - $attributes = array_merge(array('tabindex="10"'), $attributes); + $attributes = array_merge(array('tabindex="11"'), $attributes); $html = $this->helper->form->label(t('Time spent'), 'time_spent'); $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes); @@ -142,8 +202,8 @@ class TaskHelper extends Base public function selectStartDate(array $values, array $errors = array(), array $attributes = array()) { - $placeholder = date($this->config->get('application_date_format', 'm/d/Y H:i')); - $attributes = array_merge(array('tabindex="11"', 'placeholder="'.$placeholder.'"'), $attributes); + $placeholder = date($this->configModel->get('application_date_format', 'm/d/Y H:i')); + $attributes = array_merge(array('tabindex="12"', 'placeholder="'.$placeholder.'"'), $attributes); $html = $this->helper->form->label(t('Start Date'), 'date_started'); $html .= $this->helper->form->text('date_started', $values, $errors, $attributes, 'form-datetime'); @@ -153,8 +213,8 @@ class TaskHelper extends Base public function selectDueDate(array $values, array $errors = array(), array $attributes = array()) { - $placeholder = date($this->config->get('application_date_format', 'm/d/Y')); - $attributes = array_merge(array('tabindex="12"', 'placeholder="'.$placeholder.'"'), $attributes); + $placeholder = date($this->configModel->get('application_date_format', 'm/d/Y')); + $attributes = array_merge(array('tabindex="13"', 'placeholder="'.$placeholder.'"'), $attributes); $html = $this->helper->form->label(t('Due Date'), 'date_due'); $html .= $this->helper->form->text('date_due', $values, $errors, $attributes, 'form-date'); @@ -178,9 +238,9 @@ class TaskHelper extends Base public function getProgress($task) { if (! isset($this->columns[$task['project_id']])) { - $this->columns[$task['project_id']] = $this->column->getList($task['project_id']); + $this->columns[$task['project_id']] = $this->columnModel->getList($task['project_id']); } - return $this->task->getProgress($task, $this->columns[$task['project_id']]); + return $this->taskModel->getProgress($task, $this->columns[$task['project_id']]); } } diff --git a/sources/app/Helper/TextHelper.php b/sources/app/Helper/TextHelper.php index e5aefdc..654eb17 100644 --- a/sources/app/Helper/TextHelper.php +++ b/sources/app/Helper/TextHelper.php @@ -27,17 +27,29 @@ class TextHelper extends Base /** * Markdown transformation * - * @param string $text Markdown content - * @param array $link Link parameters for replacement + * @param string $text + * @param boolean $isPublicLink * @return string */ - public function markdown($text, array $link = array()) + public function markdown($text, $isPublicLink = false) { - $parser = new Markdown($this->container, $link); + $parser = new Markdown($this->container, $isPublicLink); $parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML); return $parser->text($text); } + /** + * Escape Markdown text that need to be stored in HTML attribute + * + * @access public + * @param string $text + * @return mixed + */ + public function markdownAttribute($text) + { + return htmlentities($this->markdown($text), ENT_QUOTES, 'UTF-8'); + } + /** * Format a file size * diff --git a/sources/app/Helper/UrlHelper.php b/sources/app/Helper/UrlHelper.php index 095c4af..2127c69 100644 --- a/sources/app/Helper/UrlHelper.php +++ b/sources/app/Helper/UrlHelper.php @@ -25,7 +25,7 @@ class UrlHelper extends Base */ public function doc($label, $file) { - return $this->link($label, 'doc', 'show', array('file' => $file), false, '', '', true); + return $this->link($label, 'DocumentationController', 'show', array('file' => $file), false, '', '', true); } /** @@ -109,7 +109,7 @@ class UrlHelper extends Base public function base() { if (empty($this->base)) { - $this->base = $this->config->get('application_url') ?: $this->server(); + $this->base = $this->configModel->get('application_url') ?: $this->server(); } return $this->base; diff --git a/sources/app/Helper/UserHelper.php b/sources/app/Helper/UserHelper.php index c3369df..ab259a6 100644 --- a/sources/app/Helper/UserHelper.php +++ b/sources/app/Helper/UserHelper.php @@ -3,6 +3,7 @@ namespace Kanboard\Helper; use Kanboard\Core\Base; +use Kanboard\Core\Security\Role; /** * User helpers @@ -20,7 +21,7 @@ class UserHelper extends Base */ public function hasNotifications() { - return $this->userUnreadNotification->hasNotifications($this->userSession->getId()); + return $this->userUnreadNotificationModel->hasNotifications($this->userSession->getId()); } /** @@ -35,10 +36,21 @@ class UserHelper extends Base $initials = ''; foreach (explode(' ', $name, 2) as $string) { - $initials .= mb_substr($string, 0, 1); + $initials .= mb_substr($string, 0, 1, 'UTF-8'); } - return mb_strtoupper($initials); + return mb_strtoupper($initials, 'UTF-8'); + } + + /** + * Return the user full name + * + * @param array $user User properties + * @return string + */ + public function getFullname(array $user = array()) + { + return $this->userModel->getFullname(empty($user) ? $this->userSession->getAll() : $user); } /** @@ -95,6 +107,10 @@ class UserHelper extends Base */ public function hasAccess($controller, $action) { + if (! $this->userSession->isLogged()) { + return false; + } + $key = 'app_access:'.$controller.$action; $result = $this->memoryCache->get($key); @@ -116,6 +132,10 @@ class UserHelper extends Base */ public function hasProjectAccess($controller, $action, $project_id) { + if (! $this->userSession->isLogged()) { + return false; + } + if ($this->userSession->isAdmin()) { return true; } @@ -145,17 +165,28 @@ class UserHelper extends Base */ public function getProjectUserRole($project_id) { - return $this->memoryCache->proxy($this->projectUserRole, 'getUserRole', $project_id, $this->userSession->getId()); + return $this->memoryCache->proxy($this->projectUserRoleModel, 'getUserRole', $project_id, $this->userSession->getId()); } /** - * Return the user full name + * Return true if the user can remove a task * - * @param array $user User properties - * @return string + * Regular users can't remove tasks from other people + * + * @public + * @param array $task + * @return bool */ - public function getFullname(array $user = array()) + public function canRemoveTask(array $task) { - return $this->user->getFullname(empty($user) ? $this->userSession->getAll() : $user); + if (isset($task['creator_id']) && $task['creator_id'] == $this->userSession->getId()) { + return true; + } + + if ($this->userSession->isAdmin() || $this->getProjectUserRole($task['project_id']) === Role::PROJECT_MANAGER) { + return true; + } + + return false; } } diff --git a/sources/app/Import/TaskImport.php b/sources/app/Import/TaskImport.php index 2abafe1..f5ca9b0 100644 --- a/sources/app/Import/TaskImport.php +++ b/sources/app/Import/TaskImport.php @@ -69,7 +69,7 @@ class TaskImport extends Base $row = $this->prepare($row); if ($this->validateCreation($row)) { - if ($this->taskCreation->create($row) > 0) { + if ($this->taskCreationModel->create($row) > 0) { $this->logger->debug('TaskImport: imported successfully line '.$line_number); $this->counter++; } else { @@ -100,27 +100,27 @@ class TaskImport extends Base $values['time_spent'] = (float) $row['time_spent']; if (! empty($row['assignee'])) { - $values['owner_id'] = $this->user->getIdByUsername($row['assignee']); + $values['owner_id'] = $this->userModel->getIdByUsername($row['assignee']); } if (! empty($row['creator'])) { - $values['creator_id'] = $this->user->getIdByUsername($row['creator']); + $values['creator_id'] = $this->userModel->getIdByUsername($row['creator']); } if (! empty($row['color'])) { - $values['color_id'] = $this->color->find($row['color']); + $values['color_id'] = $this->colorModel->find($row['color']); } if (! empty($row['column'])) { - $values['column_id'] = $this->column->getColumnIdByTitle($this->projectId, $row['column']); + $values['column_id'] = $this->columnModel->getColumnIdByTitle($this->projectId, $row['column']); } if (! empty($row['category'])) { - $values['category_id'] = $this->category->getIdByName($this->projectId, $row['category']); + $values['category_id'] = $this->categoryModel->getIdByName($this->projectId, $row['category']); } if (! empty($row['swimlane'])) { - $values['swimlane_id'] = $this->swimlane->getIdByName($this->projectId, $row['swimlane']); + $values['swimlane_id'] = $this->swimlaneModel->getIdByName($this->projectId, $row['swimlane']); } if (! empty($row['date_due'])) { diff --git a/sources/app/Import/UserImport.php b/sources/app/Import/UserImport.php index 64300d7..304a325 100644 --- a/sources/app/Import/UserImport.php +++ b/sources/app/Import/UserImport.php @@ -2,7 +2,7 @@ namespace Kanboard\Import; -use Kanboard\Model\User; +use Kanboard\Model\UserModel; use SimpleValidator\Validator; use SimpleValidator\Validators; use Kanboard\Core\Security\Role; @@ -56,7 +56,7 @@ class UserImport extends Base $row = $this->prepare($row); if ($this->validateCreation($row)) { - if ($this->user->create($row)) { + if ($this->userModel->create($row) !== false) { $this->logger->debug('UserImport: imported successfully line '.$line_number); $this->counter++; } else { @@ -109,7 +109,7 @@ class UserImport extends Base { $v = new Validator($values, array( new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50), - new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), User::TABLE, 'id'), + new Validators\Unique('username', t('The username must be unique'), $this->db->getConnection(), UserModel::TABLE, 'id'), new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6), new Validators\Email('email', t('Email address invalid')), new Validators\Integer('is_ldap_user', t('This value must be an integer')), diff --git a/sources/app/Job/BaseJob.php b/sources/app/Job/BaseJob.php new file mode 100644 index 0000000..60522ac --- /dev/null +++ b/sources/app/Job/BaseJob.php @@ -0,0 +1,33 @@ +jobParams; + } +} diff --git a/sources/app/Job/EmailJob.php b/sources/app/Job/EmailJob.php new file mode 100644 index 0000000..2da3ca2 --- /dev/null +++ b/sources/app/Job/EmailJob.php @@ -0,0 +1,55 @@ +jobParams = array($email, $name, $subject, $html, $author); + return $this; + } + + /** + * Execute job + * + * @access public + * @param string $email + * @param string $name + * @param string $subject + * @param string $html + * @param string $author + */ + public function execute($email, $name, $subject, $html, $author) + { + $transport = $this->helper->mail->getMailTransport(); + $this->logger->debug(__METHOD__.' Sending email to: '.$email.' using transport: '.$transport); + $startTime = microtime(true); + + $this->emailClient + ->getTransport($transport) + ->sendEmail($email, $name, $subject, $html, $author) + ; + + if (DEBUG) { + $this->logger->debug('Email sent in '.round(microtime(true) - $startTime, 6).' seconds'); + } + } +} diff --git a/sources/app/Job/HttpAsyncJob.php b/sources/app/Job/HttpAsyncJob.php new file mode 100644 index 0000000..9e5cf10 --- /dev/null +++ b/sources/app/Job/HttpAsyncJob.php @@ -0,0 +1,43 @@ +jobParams = array($method, $url, $content, $headers); + return $this; + } + + /** + * Set job parameters + * + * @access public + * @param string $method + * @param string $url + * @param string $content + * @param array $headers + * @return $this + */ + public function execute($method, $url, $content, array $headers) + { + $this->httpClient->doRequest($method, $url, $content, $headers); + } +} diff --git a/sources/app/Job/NotificationJob.php b/sources/app/Job/NotificationJob.php new file mode 100644 index 0000000..904a927 --- /dev/null +++ b/sources/app/Job/NotificationJob.php @@ -0,0 +1,85 @@ +jobParams = array($event->getAll(), $eventName, $eventObjectName); + return $this; + } + + /** + * Execute job + * + * @param array $event + * @param string $eventName + * @param string $eventObjectName + */ + public function execute(array $event, $eventName, $eventObjectName) + { + $eventData = $this->getEventData($event, $eventObjectName); + + if (! empty($eventData)) { + if (! empty($event['mention'])) { + $this->userNotificationModel->sendUserNotification($event['mention'], $eventName, $eventData); + } else { + $this->userNotificationModel->sendNotifications($eventName, $eventData); + $this->projectNotificationModel->sendNotifications($eventData['task']['project_id'], $eventName, $eventData); + } + } + } + + /** + * Get event data + * + * @param array $event + * @param string $eventObjectName + * @return array + */ + public function getEventData(array $event, $eventObjectName) + { + $values = array(); + + if (! empty($event['changes'])) { + $values['changes'] = $event['changes']; + } + + switch ($eventObjectName) { + case 'Kanboard\Event\TaskEvent': + $values['task'] = $this->taskFinderModel->getDetails($event['task_id']); + break; + case 'Kanboard\Event\SubtaskEvent': + $values['subtask'] = $this->subtaskModel->getById($event['id'], true); + $values['task'] = $this->taskFinderModel->getDetails($values['subtask']['task_id']); + break; + case 'Kanboard\Event\FileEvent': + $values['file'] = $event; + $values['task'] = $this->taskFinderModel->getDetails($values['file']['task_id']); + break; + case 'Kanboard\Event\CommentEvent': + $values['comment'] = $this->commentModel->getById($event['id']); + $values['task'] = $this->taskFinderModel->getDetails($values['comment']['task_id']); + break; + } + + return $values; + } +} diff --git a/sources/app/Job/ProjectMetricJob.php b/sources/app/Job/ProjectMetricJob.php new file mode 100644 index 0000000..6330bd4 --- /dev/null +++ b/sources/app/Job/ProjectMetricJob.php @@ -0,0 +1,40 @@ +jobParams = array($projectId); + return $this; + } + + /** + * Execute job + * + * @access public + * @param integer $projectId + */ + public function execute($projectId) + { + $this->logger->debug(__METHOD__.' Run project metrics calculation'); + $now = date('Y-m-d'); + + $this->projectDailyColumnStatsModel->updateTotals($projectId, $now); + $this->projectDailyStatsModel->updateTotals($projectId, $now); + } +} diff --git a/sources/app/Locale/bs_BA/translations.php b/sources/app/Locale/bs_BA/translations.php index 7ca864f..4213d10 100644 --- a/sources/app/Locale/bs_BA/translations.php +++ b/sources/app/Locale/bs_BA/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Pregledaj zadatak', 'Remove user' => 'Ukloni korisnika', 'Do you really want to remove this user: "%s"?' => 'Da li zaista želiš da ukloniš korisnika: "%s"?', - 'New user' => 'Novi korisnik', 'All users' => 'Svi korisnici', 'Username' => 'Korisničko ime', 'Password' => 'Šifra', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d zatvorenih zadataka', 'No task for this project' => 'Nema dodijeljenih zadataka ovom projektu', 'Public link' => 'Javni link', - 'Change assignee' => 'Promjena izvršioca', - 'Change assignee for the task "%s"' => 'Promjena izvršioca za zadatak "%s"', 'Timezone' => 'Vremenska zona', 'Sorry, I didn\'t find this information in my database!' => 'Na žalost, nije pronađena informacija u bazi', 'Page not found' => 'Strana nije pronađena', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Dodaj opis', 'Comment added successfully.' => 'Komentar uspješno dodan', 'Unable to create your comment.' => 'Nemoguće kreiranje komentara', - 'Edit this task' => 'Uredi ovaj zadatak', 'Due Date' => 'Treba biti gotovo do dana', 'Invalid date' => 'Pogrešan datum', 'Automatic actions' => 'Automatske akcije', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategorija', 'Category:' => 'Kategorija:', 'Categories' => 'Kategorije', - 'Category not found.' => 'Kategorija nije pronađena', 'Your category have been created successfully.' => 'Uspješno kreirana kategorija.', 'Unable to create your category.' => 'Nije moguće kreirati kategoriju.', 'Your category have been updated successfully.' => 'Kategorija je uspješno ažurirana', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Da li da uklonim fajl: "%s"?', 'Attachments' => 'Prilozi', 'Edit the task' => 'Uredi zadatak', - 'Edit the description' => 'Uredi opis zadatka', 'Add a comment' => 'Dodaj komentar', 'Edit a comment' => 'Izmijeni komentar', 'Summary' => 'Pregled', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Bez omogućenih vanjskih autentikacija.', 'Password modified successfully.' => 'Uspješna izmjena šifre.', 'Unable to change the password.' => 'Nije moguće izmijeniti šifru.', - 'Change category for the task "%s"' => 'Izijmeni kategoriju zadatka "%s"', 'Change category' => 'Izmijeni kategoriju', '%s updated the task %s' => '%s izmijenio zadatak %s', '%s opened the task %s' => '%s otvorio zadatak %s', @@ -413,9 +406,7 @@ return array( 'About' => 'O Kanboardu', 'Database driver:' => 'Database driver:', 'Board settings' => 'Postavke table', - 'URL and token' => 'URL i token', 'Webhook settings' => 'Postavke za webhook', - 'URL for task creation:' => 'URL za kreiranje zadataka', 'Reset token' => 'Resetuj token', 'API endpoint:' => 'API endpoint', 'Refresh interval for private board' => 'Interval osvježavanja privatnih ploča', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Ukloni swimline traku', 'Show default swimlane' => 'Prikaži podrazumijevanu swimline traku', 'Swimlane modification for the project "%s"' => 'Izmjene swimline trake za projekat "%s"', - 'Swimlane not found.' => 'Swimline traka nije pronađena.', 'Swimlane removed successfully.' => 'Swimline traka uspješno uklonjena.', 'Swimlanes' => 'Swimline trake', 'Swimlane updated successfully.' => 'Swimline traka uspjeno ažurirana.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Sve swimline trake', 'All colors' => 'Sve boje', 'Moved to column %s' => 'Premješten u kolonu %s', - 'Change description' => 'Promijeni opis', 'User dashboard' => 'Korisnički panel', 'Allow only one subtask in progress at the same time for a user' => 'Dozvoli samo jedan pod-zadatak "u radu" po korisniku', 'Edit column "%s"' => 'Uredi kolonu "%s"', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'pregled ploče na Kanboard-u', 'The task have been moved to the first swimlane' => 'Zadatak je premješten u prvu swimline traku', 'The task have been moved to another swimlane:' => 'Zadatak je premješten u drugu swimline traku', - 'Overdue tasks for the project "%s"' => 'Zadaci u kašnjenju za projekat "%s"', 'New title: %s' => 'Novi naslov: %s', 'The task is not assigned anymore' => 'Zadatak nema više izvršioca', 'New assignee: %s' => 'Novi izvršilac: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Zaustavi tajmer', 'Start timer' => 'Pokreni tajmer', 'Add project member' => 'Dodaj člana projekta', - 'Enable notifications' => 'Omogući obavještenja', 'My activity stream' => 'Tok mojih aktivnosti', 'My calendar' => 'Moj kalendar', 'Search tasks' => 'Pretraga zadataka', @@ -763,14 +750,14 @@ return array( 'List' => 'Lista', 'Filter' => 'Filter', 'Advanced search' => 'Napredna pretraga', - 'Example of query: ' => 'Primjer za upit', - 'Search by project: ' => 'Pretraga po projektu', - 'Search by column: ' => 'Pretraga po koloni', - 'Search by assignee: ' => 'Pretraga po izvršiocu', - 'Search by color: ' => 'Pretraga po boji', - 'Search by category: ' => 'Pretraga po kategoriji', - 'Search by description: ' => 'Pretraga po opisu', - 'Search by due date: ' => 'Pretraga po datumu završetka', + 'Example of query: ' => 'Primjer za upit: ', + 'Search by project: ' => 'Pretraga po projektu: ', + 'Search by column: ' => 'Pretraga po koloni: ', + 'Search by assignee: ' => 'Pretraga po izvršiocu: ', + 'Search by color: ' => 'Pretraga po boji: ', + 'Search by category: ' => 'Pretraga po kategoriji: ', + 'Search by description: ' => 'Pretraga po opisu: ', + 'Search by due date: ' => 'Pretraga po datumu završetka: ', 'Lead and Cycle time for "%s"' => 'Vrijeme upravljanje i vremenski ciklus za "%s"', 'Average time spent into each column for "%s"' => 'Prosjek utrošenog vremena u svakoj koloni za "%s"', 'Average time spent into each column' => 'Prosjek utrošenog vrmena u svakoj koloni', @@ -1116,7 +1103,7 @@ return array( 'Column created successfully.' => 'Kolona uspješno napravljena.', 'Another column with the same name exists in the project' => 'Već postoji kolona s istim imenom u ovom projektu.', 'Default filters' => 'Podrazumijevani filteri', - 'Your board doesn\'t have any column!' => 'Vaš panel nema ni jednu kolonu!', + 'Your board doesn\'t have any columns!' => 'Vaš panel nema ni jednu kolonu!', 'Change column position' => 'Promijeni poziciju kolone', 'Switch to the project overview' => 'Promijeni u pregled projekta', 'User filters' => 'Korisnički filteri', @@ -1135,22 +1122,98 @@ return array( 'There is no action at the moment.' => 'Trenutno nema akcija.', 'Import actions from another project' => 'Uvezi akcije iz drugog projekta', 'There is no available project.' => 'Trenutno nema dostupnih projekata.', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', - // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', - // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', - // 'Menu' => '', - // 'Set start date' => '', - // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'Local File' => 'Lokalni fajl', + 'Configuration' => 'Konfiguracija', + 'PHP version:' => 'Verzija PHP-a:', + 'PHP SAPI:' => 'Verzija SAPI-a:', + 'OS version:' => 'Verzija OS-a:', + 'Database version:' => 'Verzija baze podataka:', + 'Browser:' => 'Pretraživač:', + 'Task view' => 'Pregled zadatka', + 'Edit task' => 'Uredi zadatak', + 'Edit description' => 'Uredi opis', + 'New internal link' => 'Nova unutrašnja veza', + 'Display list of keyboard shortcuts' => 'Prikaži listu prečica na tastaturi', + 'Menu' => 'Meni', + 'Set start date' => 'Postavi početni datum', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Dodaj sliku za moj avatar', + 'Remove my image' => 'Ukloni moju sliku', + 'The OAuth2 state parameter is invalid' => 'OAuth2 status parametar nije validan', + 'User not found.' => 'Korisnik nije pronađen.', + 'Search in activity stream' => 'Pretraži aktivnosti', + 'My activities' => 'Moje aktivnosti', + 'Activity until yesterday' => 'Aktivnosti do jučer', + 'Activity until today' => 'Aktivnosti do danas', + 'Search by creator: ' => 'Pretraga po kreatoru: ', + 'Search by creation date: ' => 'Pretraga po datumu kreiranja: ', + 'Search by task status: ' => 'Pretraga po statusu zadatka: ', + 'Search by task title: ' => 'Pretraga po naslovu zadatka: ', + 'Activity stream search' => 'Pretraga aktivnosti', + 'Projects where "%s" is manager' => 'Projekti gdje je "%s" menadžer', + 'Projects where "%s" is member' => 'Projekti gdje je "%s" član', + 'Open tasks assigned to "%s"' => 'Otvoreni zadaci dodijeljeni "%s"', + 'Closed tasks assigned to "%s"' => 'Zatvoreni zadaci dodijeljeni "%s"', + // 'Assign automatically a color based on a priority' => '', + 'Overdue tasks for the project(s) "%s"' => 'Zadaci u kašnjenju za projekat(te) "%s"', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/cs_CZ/translations.php b/sources/app/Locale/cs_CZ/translations.php index b2921de..78df7ec 100644 --- a/sources/app/Locale/cs_CZ/translations.php +++ b/sources/app/Locale/cs_CZ/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Zobrazit úkol', 'Remove user' => 'Odebrat uživatele', 'Do you really want to remove this user: "%s"?' => 'Opravdu chcete odebrat uživatele: "%s"?', - 'New user' => 'Nový uživatel', 'All users' => 'Všichni uživatelé', 'Username' => 'Uživatelské jméno', 'Password' => 'Heslo', @@ -101,7 +100,7 @@ return array( 'There is nobody assigned' => 'Není přiřazeno žádnému uživateli', 'Column on the board:' => 'Sloupec:', 'Close this task' => 'Uzavřít úkol', - 'Open this task' => 'Aufgabe wieder öffnen', + 'Open this task' => 'Otevřít tento úkol', 'There is no description.' => 'Bez popisu', 'Add a new task' => 'Přidat nový úkol', 'The username is required' => 'Uživatelské jméno je vyžadováno', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d dokončených úkolů', 'No task for this project' => 'Tento projekt nemá žádné úkoly', 'Public link' => 'Veřejný odkaz', - 'Change assignee' => 'Změna přiřazení k uživatelům', - 'Change assignee for the task "%s"' => 'Změna přiřazení uživatele pro úkol "%s"', 'Timezone' => 'Časová zóna', 'Sorry, I didn\'t find this information in my database!' => 'Omlouváme se, tuto informaci nelze nalézt!', 'Page not found' => 'Stránka nenalezena', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Vložte popis', 'Comment added successfully.' => 'Komentář byl úspěšně přidán.', 'Unable to create your comment.' => 'Komentář nelze vytvořit.', - 'Edit this task' => 'Editace úkolu', 'Due Date' => 'Datum splnění', 'Invalid date' => 'Neplatné datum', 'Automatic actions' => 'Automaticky vykonávané akce', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategorie', 'Category:' => 'Kategorie:', 'Categories' => 'Kategorie', - 'Category not found.' => 'Kategorie není nalezena.', 'Your category have been created successfully.' => 'Kategorie byla úspěšně vytvořena.', 'Unable to create your category.' => 'Kategorii nelze vytvořit.', 'Your category have been updated successfully.' => 'Kategorie byla úspěšně aktualizována.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Skutečně chcete odebrat soubor: "%s"?', 'Attachments' => 'Přílohy', 'Edit the task' => 'Upravit úkol', - 'Edit the description' => 'Upravit popis', 'Add a comment' => 'Přidat komentář', 'Edit a comment' => 'Upravit komentář', 'Summary' => 'Souhrn', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Není povolena žádná vzdálená autorizace.', 'Password modified successfully.' => 'Heslo bylo úspěšně změněno.', 'Unable to change the password.' => 'Nelze změnit heslo.', - 'Change category for the task "%s"' => 'Změna kategorie pro úkol "%s" ', 'Change category' => 'Změna kategorie', '%s updated the task %s' => '%s aktualizoval úkol %s ', '%s opened the task %s' => '%s znovu otevřel úkol %s ', @@ -400,8 +393,8 @@ return array( 'Default values are "%s"' => 'Standardní hodnoty jsou: "%s"', 'Default columns for new projects (Comma-separated)' => 'Výchozí sloupce pro nové projekty (odděleny čárkou)', 'Task assignee change' => 'Změna přiřazení uživatelů', - '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', - '%s changed the assignee of the task %s to %s' => '%s změnil řešitele úkolu %s na uživatele %s', + '%s change the assignee of the task #%d to %s' => '%s změnil přidělení úkolu #%d na uživatele %s', + '%s changed the assignee of the task %s to %s' => '%s změnil přidělení úkolu %s na uživatele %s', 'New password for the user "%s"' => 'Nové heslo pro uživatele "%s"', 'Choose an event' => 'Vybrat událost', 'Create a task from an external provider' => 'Vytvořit úkol externím poskytovatelem', @@ -413,9 +406,7 @@ return array( 'About' => 'O projektu', 'Database driver:' => 'Databáze', 'Board settings' => 'Nastavení nástěnky', - 'URL and token' => 'URL a Token', 'Webhook settings' => 'Webhook nastavení', - 'URL for task creation:' => 'URL pro vytvoření úkolu', 'Reset token' => 'Token reset', 'API endpoint:' => 'API endpoint', 'Refresh interval for private board' => 'Interval automatického obnovování pro soukromé nástěnky', @@ -466,13 +457,13 @@ return array( 'The project id must be an integer' => 'ID projektu musí být celé číslo', 'The status must be an integer' => 'Status musí být celé číslo', 'The subtask id is required' => 'Je požadováno id dílčího úkolu', - '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', + 'The subtask id must be an integer' => 'ID dílčího úkolu musí být číslo', + 'The task id is required' => 'ID úkolu je povinné', + 'The task id must be an integer' => 'ID úkolu musí být číslo', + 'The user id must be an integer' => 'ID uživatele musí být číslo', + 'This value is required' => 'Hodnota je povinná', + 'This value must be numeric' => 'Hodnota musí být číselná', + 'Unable to create this task.' => 'Nelze vytvořit tento úkol', 'Cumulative flow diagram' => 'Kumulativní diagram', 'Cumulative flow diagram for "%s"' => 'Kumulativní diagram pro "%s"', 'Daily project summary' => 'Denní přehledy', @@ -480,27 +471,26 @@ return array( 'Daily project summary export for "%s"' => 'Export denních přehledů pro "%s"', 'Exports' => 'Exporty', 'This export contains the number of tasks per column grouped per day.' => 'Tento export obsahuje počet úkolů pro jednotlivé sloupce seskupených podle dní.', - 'Active swimlanes' => 'Aktive Swimlane', - 'Add a new swimlane' => 'Přidat nový řádek', - 'Change default swimlane' => 'Standard Swimlane ändern', - 'Default swimlane' => 'Výchozí Swimlane', - 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', - 'Inactive swimlanes' => 'Inaktive Swimlane', - 'Remove a swimlane' => 'Odstranit swimlane', - '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 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"', + 'Active swimlanes' => 'Aktivní dráhy', + 'Add a new swimlane' => 'Přidat novou dráhu', + 'Change default swimlane' => 'Změnit výchozí dráhu', + 'Default swimlane' => 'Výchozí dráha', + 'Do you really want to remove this swimlane: "%s"?' => 'Opravdu si přejete odstranit tuto dráhu: "%s"?', + 'Inactive swimlanes' => 'Neaktivní dráha', + 'Remove a swimlane' => 'Odstranit dráhu', + 'Show default swimlane' => 'Zobrazit výchozí dráhu', + 'Swimlane modification for the project "%s"' => 'Změny dráhy pro projekt "%s"', + 'Swimlane removed successfully.' => 'Dráha byla odstraněna.', + 'Swimlanes' => 'Dráhy', + 'Swimlane updated successfully.' => 'Dráha byla upravena.', + 'The default swimlane have been updated successfully.' => 'Výchozí dráha byla upravena', + 'Unable to remove this swimlane.' => 'Tuto dráhu nelze odstranit.', + 'Unable to update this swimlane.' => 'Tuto dráhu nelze upravit.', + 'Your swimlane have been created successfully.' => 'Dráha byla vytvořena.', + 'Example: "Bug, Feature Request, Improvement"' => 'Například: "Chyba", "Nápad", "Požadavek"...', 'Default categories for new projects (Comma-separated)' => 'Výchozí kategorie pro nové projekty (oddělené čárkou)', 'Integrations' => 'Integrace', - 'Integration with third-party services' => 'Integration von Fremdleistungen', + 'Integration with third-party services' => 'Integrace se službami třetích stran', 'Subtask Id' => 'Dílčí úkol Id', 'Subtasks' => 'Dílčí úkoly', 'Subtasks Export' => 'Export dílčích úkolů', @@ -517,11 +507,10 @@ return array( 'All swimlanes' => 'Alle Swimlanes', 'All colors' => 'Všechny barvy', 'Moved to column %s' => 'Přesunuto do sloupce %s ', - 'Change description' => 'Změna podrobného popisu', 'User dashboard' => 'Nástěnka uživatele', 'Allow only one subtask in progress at the same time for a user' => 'Umožnit uživateli práci pouze na jednom dílčím úkolu ve stejném čase', 'Edit column "%s"' => 'Upravit sloupec "%s" ', - 'Select the new status of the subtask: "%s"' => 'Wähle einen neuen Status für Teilaufgabe: "%s"', + 'Select the new status of the subtask: "%s"' => 'Vyberte nový stav pro podúkol: "%s"', 'Subtask timesheet' => 'Časový rozvrh dílčích úkolů', 'There is nothing to show.' => 'Žádná položka k zobrazení', 'Time Tracking' => 'Sledování času', @@ -534,9 +523,9 @@ return array( 'Days in this column' => 'Dní v tomto sloupci', '%dd' => '%d d', 'Add a new link' => 'Přidat nový odkaz', - 'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?', - 'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?', - 'Field required' => 'Feld erforderlich', + 'Do you really want to remove this link: "%s"?' => 'Opravdu chcete odstranit odkaz "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Opravdu chcete odstranit odkaz na úkol #%d ?', + 'Field required' => 'Povinné pole', 'Link added successfully.' => 'Propojení bylo úspěšně přidáno.', 'Link updated successfully.' => 'Propojení bylo úspěšně aktualizováno.', 'Link removed successfully.' => 'Propojení bylo úspěšně odebráno.', @@ -560,8 +549,8 @@ return array( 'is duplicated by' => 'je duplikován', 'is a child of' => 'je podřízený', 'is a parent of' => 'je nadřízený', - 'targets milestone' => 'targets milestone', - 'is a milestone of' => 'is a milestone of', + 'targets milestone' => 'patří k milníku', + 'is a milestone of' => 'je milníkem', 'fixes' => 'nahrazuje', 'is fixed by' => 'je nahrazen', 'This task' => 'Tento úkol', @@ -603,7 +592,7 @@ return array( 'Time spent in the column' => 'Trvání jednotlivých etap', 'Task transitions' => 'Přesuny úkolů', 'Task transitions export' => 'Export přesunů mezi sloupci', - '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.', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Tento seznam obsahuje všechny pohyby úkolů s daty, uživateli a časy strávenými na úkolu.', 'Currency rates' => 'Aktuální kurzy', 'Rate' => 'Kurz', 'Change reference currency' => 'Změnit referenční měnu', @@ -615,28 +604,28 @@ return array( '%s remove the assignee of the task %s' => '%s odstranil přiřazení úkolu %s ', 'Enable Gravatar images' => 'Aktiviere Gravatar Bilder', 'Information' => 'Informace', - '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', + 'Check two factor authentication code' => 'Zkontrolujte dvouúrovňový autentifikační klíč', + 'The two factor authentication code is not valid.' => 'Dvouúrovňový autentifikační klíč není platný.', + 'The two factor authentication code is valid.' => 'Dvouúrovňový autentifikační klíč je platný.', + 'Code' => 'Klíč', 'Two factor authentication' => 'Dvouúrovňová autorizace', - 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', + 'This QR code contains the key URI: ' => 'Tento QR kód obsahuje adresu s klíčem', 'Check my code' => 'Kontrola mého kódu', 'Secret key: ' => 'Tajný klíč', 'Test your device' => 'Test Vašeho zařízení', 'Assign a color when the task is moved to a specific column' => 'Přiřadit barvu, když je úkol přesunut do konkrétního sloupce', '%s via Kanboard' => '%s via Kanboard', - 'Burndown chart for "%s"' => 'Burndown-Chart für "%s"', + 'Burndown chart for "%s"' => 'Burndown-Chart pro "%s"', 'Burndown chart' => 'Burndown-Chart', 'This chart show the task complexity over the time (Work Remaining).' => 'Graf zobrazuje složitost úkolů v čase (Zbývající práce).', 'Screenshot taken %s' => 'Screenshot aufgenommen %s ', 'Add a screenshot' => 'Přidat snímek obrazovky', - '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.', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Pořiďte snímek obrazovky a v tomto poli stiskněte Ctrl+V nebo ⌘+V ', + 'Screenshot uploaded successfully.' => 'Snímek obrazovky byl úspěšně nahrán.', 'SEK - Swedish Krona' => 'SEK - Schwedische Kronen', - 'Identifier' => 'Identifikator', - 'Disable two factor authentication' => 'Zrušit dvou stupňovou autorizaci', - 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Willst du wirklich für folgenden Nutzer die Zwei-Faktor-Authentifizierung deaktivieren: "%s"?', + 'Identifier' => 'Identifikátor', + 'Disable two factor authentication' => 'Zrušit dvouúrovňovou autorizaci', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Opravdu chcete vypnout dvouúrovňovou autentifikaci pro uživatele: "%s"?', // 'Edit link' => '', // 'Start to type task title...' => '', // 'A task cannot be linked to itself' => '', @@ -691,28 +680,27 @@ return array( 'Move the task to another column when the category is changed' => 'Přesun úkolu do jiného sloupce když je změněna kategorie', 'Send a task by email to someone' => 'Poslat někomu úkol poštou', 'Reopen a task' => 'Znovu otevřít úkol', - 'Column change' => 'Spalte geändert', - 'Position change' => 'Position geändert', - 'Swimlane change' => 'Swimlane geändert', - 'Assignee change' => 'Zuordnung geändert', - '[%s] Overdue tasks' => '[%s] überfallige Aufgaben', - 'Notification' => 'Benachrichtigungen', - '%s moved the task #%d to the first swimlane' => '%s hat die Aufgabe #%d in die erste Swimlane verschoben', - '%s moved the task #%d to the swimlane "%s"' => '%s hat die Aufgabe #%d in die Swimlane "%s" verschoben', + 'Column change' => 'Změna sloupce', + 'Position change' => 'Změna pozice', + 'Swimlane change' => 'Změna dráhy', + 'Assignee change' => 'Změna přidělení', + '[%s] Overdue tasks' => '[%s] přetažených úkolů', + 'Notification' => 'Upozornění', + '%s moved the task #%d to the first swimlane' => '%s přesunul úkol #%d do první dráhy', + '%s moved the task #%d to the swimlane "%s"' => '%s přesunul úkol #%d do dráhy "%s"', // 'Swimlane' => '', // 'Gravatar' => '', - '%s moved the task %s to the first swimlane' => '%s hat die Aufgabe %s in die erste Swimlane verschoben', - '%s moved the task %s to the swimlane "%s"' => '%s hat die Aufgaben %s in die Swimlane "%s" verschoben', + '%s moved the task %s to the first swimlane' => '%s přesunul úkol %s do první dráhy', + '%s moved the task %s to the swimlane "%s"' => '%s přesunul úkol %s do dráhy "%s"', 'This report contains all subtasks information for the given date range.' => 'Report obsahuje všechny informace o dílčích úkolech pro daný časový úsek', 'This report contains all tasks information for the given date range.' => 'Report obsahuje informace o všech úkolech pro daný časový úsek.', 'Project activities for %s' => 'Aktivity projektu %s', - 'view the board on Kanboard' => 'Pinnwand in Kanboard anzeigen', - 'The task have been moved to the first swimlane' => 'Die Aufgabe wurde in die erste Swimlane verschoben', - 'The task have been moved to another swimlane:' => 'Die Aufgaben wurde in ene andere Swimlane verschoben', - 'Overdue tasks for the project "%s"' => 'Überfällige Aufgaben für das Projekt "%s"', - 'New title: %s' => 'Neuer Titel: %s', - 'The task is not assigned anymore' => 'Die Aufgabe ist nicht mehr zugewiesen', - 'New assignee: %s' => 'Neue Zuordnung: %s', + 'view the board on Kanboard' => 'Zobrazit nástěnku', + 'The task have been moved to the first swimlane' => 'Úkol byl přesunut do první dráhy', + 'The task have been moved to another swimlane:' => 'Úkol byl přesunut do další dráhy', + 'New title: %s' => 'Nový název: %s', + 'The task is not assigned anymore' => 'Úkol již není přidělen', + 'New assignee: %s' => 'přidělení: %s', 'There is no category now' => 'Nyní neexistuje žádná kategorie', 'New category: %s' => 'Nová kategorie: %s', 'New color: %s' => 'Nová barva: %s', @@ -720,11 +708,11 @@ return array( 'The due date have been removed' => 'Datum dokončení byl odstraněn', 'There is no description anymore' => 'Ještě neexistuje žádný popis', 'Recurrence settings have been modified' => 'Nastavení opakování bylo změněno', - 'Time spent changed: %sh' => 'Verbrauchte Zeit geändert: %sh', - 'Time estimated changed: %sh' => 'Geschätzte Zeit geändert: %sh', - 'The field "%s" have been updated' => 'Das Feld "%s" wurde verändert', - 'The description has been modified:' => 'Die Beschreibung wurde geändert:', - 'Do you really want to close the task "%s" as well as all subtasks?' => 'Soll die Aufgabe "%s" wirklich geschlossen werden? (einschließlich Teilaufgaben)', + 'Time spent changed: %sh' => 'Strávený čas se změnil: %sh', + 'Time estimated changed: %sh' => 'Odhadovaný čas se změnil: %sh', + 'The field "%s" have been updated' => 'Sloupec "%s" byl upraven', + 'The description has been modified:' => 'Popis byl upraven:', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Opravdu si přejete úkol "%s" uzavřít? (včetně podúkolů)', 'I want to receive notifications for:' => 'Chci dostávat upozornění na:', 'All tasks' => 'Všechny úkoly', 'Only for tasks assigned to me' => 'pouze pro moje úkoly', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Zastavit časovač', 'Start timer' => 'Spustit časovač', 'Add project member' => 'Přidat člena projektu', - 'Enable notifications' => 'Povolit notifikace', 'My activity stream' => 'Přehled mých aktivit', 'My calendar' => 'Můj kalendář', 'Search tasks' => 'Hledání úkolů', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/da_DK/translations.php b/sources/app/Locale/da_DK/translations.php index c474392..bf595a0 100644 --- a/sources/app/Locale/da_DK/translations.php +++ b/sources/app/Locale/da_DK/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Se denne opgave', 'Remove user' => 'Fjern bruger', 'Do you really want to remove this user: "%s"?' => 'Ønsker du virkelig at fjerne denne bruger: "%s"?', - 'New user' => 'Ny bruger', 'All users' => 'Alle brugere', 'Username' => 'Brugernavn', 'Password' => 'Password', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d lukket opgavet', 'No task for this project' => 'Ingen opgaver i dette projekt', 'Public link' => 'Offentligt link', - 'Change assignee' => 'Ændre ansvarlig', - 'Change assignee for the task "%s"' => 'Ændre ansvarlig for opgaven: "%s"', 'Timezone' => 'Tidszone', 'Sorry, I didn\'t find this information in my database!' => 'Denne information kunne ikke findes i databasen!', 'Page not found' => 'Siden er ikke fundet', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Efterlad en beskrivelse...', 'Comment added successfully.' => 'Kommentaren er tilføjet.', 'Unable to create your comment.' => 'Din kommentar kunne ikke oprettes.', - 'Edit this task' => 'Rediger denne opgave', 'Due Date' => 'Forfaldsdato', 'Invalid date' => 'Ugyldig dato', 'Automatic actions' => 'Automatiske handlinger', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategori', 'Category:' => 'Kategori:', 'Categories' => 'Kategorier', - 'Category not found.' => 'Kategori ikke fundet.', 'Your category have been created successfully.' => 'Kategorien er oprettet.', 'Unable to create your category.' => 'Kategorien kunne ikke oprettes.', 'Your category have been updated successfully.' => 'Kategorien er opdateret.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Vil du virkelig fjerne denne fil: "%s"?', 'Attachments' => 'Vedhæftninger', 'Edit the task' => 'Rediger opgaven', - 'Edit the description' => 'Rediger beskrivelsen', 'Add a comment' => 'Tilføj en kommentar', 'Edit a comment' => 'Rediger en kommentar', 'Summary' => 'Resumé', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Ingen eksterne autentificering aktiveret.', 'Password modified successfully.' => 'Adgangskode ændret.', '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 %s' => '%s opdatert opgaven %s', '%s opened the task %s' => '%s åben opgaven %s', @@ -413,9 +406,7 @@ return array( 'About' => 'Om', 'Database driver:' => 'Database driver:', 'Board settings' => 'Baord indstillinger', - 'URL and token' => 'URL og token', 'Webhook settings' => 'Webhook indstillinger', - 'URL for task creation:' => 'URL for opgave oprettelse:', 'Reset token' => 'Reset endpoint', 'API endpoint:' => 'API endpoint:', 'Refresh interval for private board' => 'Refresh interval for privat board', @@ -489,7 +480,6 @@ return array( // 'Remove a swimlane' => '', // 'Show default swimlane' => '', // 'Swimlane modification for the project "%s"' => '', - // 'Swimlane not found.' => '', // 'Swimlane removed successfully.' => '', // 'Swimlanes' => '', // 'Swimlane updated successfully.' => '', @@ -517,7 +507,6 @@ return array( // 'All swimlanes' => '', // 'All colors' => '', // 'Moved to column %s' => '', - // 'Change description' => '', // 'User dashboard' => '', // 'Allow only one subtask in progress at the same time for a user' => '', // 'Edit column "%s"' => '', @@ -709,7 +698,6 @@ return array( // 'view the board on Kanboard' => '', // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', // 'New title: %s' => '', // 'The task is not assigned anymore' => '', // 'New assignee: %s' => '', @@ -738,7 +726,6 @@ return array( // 'Stop timer' => '', // 'Start timer' => '', // 'Add project member' => '', - // 'Enable notifications' => '', // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/de_DE/translations.php b/sources/app/Locale/de_DE/translations.php index af88b37..81df4e5 100644 --- a/sources/app/Locale/de_DE/translations.php +++ b/sources/app/Locale/de_DE/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Aufgabe ansehen', 'Remove user' => 'Benutzer löschen', 'Do you really want to remove this user: "%s"?' => 'Soll dieser Benutzer wirklich gelöscht werden: "%s"?', - 'New user' => 'Neuer Benutzer', 'All users' => 'Alle Benutzer', 'Username' => 'Benutzername', 'Password' => 'Passwort', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d abgeschlossene Aufgaben', 'No task for this project' => 'Keine Aufgaben in diesem Projekt', 'Public link' => 'Öffentlicher Link', - '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 find this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!', 'Page not found' => 'Seite nicht gefunden', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Beschreibung eingeben', 'Comment added successfully.' => 'Kommentar erfolgreich hinzugefügt.', 'Unable to create your comment.' => 'Hinzufügen eines Kommentars nicht möglich.', - 'Edit this task' => 'Aufgabe bearbeiten', 'Due Date' => 'Fällig am', 'Invalid date' => 'Ungültiges Datum', 'Automatic actions' => 'Automatische Aktionen', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategorie', 'Category:' => 'Kategorie:', 'Categories' => 'Kategorien', - 'Category not found.' => 'Kategorie nicht gefunden.', 'Your category have been created successfully.' => 'Kategorie erfolgreich erstellt.', 'Unable to create your category.' => 'Erstellung der Kategorie nicht möglich.', 'Your category have been updated successfully.' => 'Kategorie erfolgreich aktualisiert.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: "%s"?', 'Attachments' => 'Anhänge', 'Edit the task' => 'Aufgabe bearbeiten', - 'Edit the description' => 'Beschreibung bearbeiten', 'Add a comment' => 'Kommentar hinzufügen', 'Edit a comment' => 'Kommentar bearbeiten', 'Summary' => 'Zusammenfassung', @@ -317,14 +311,14 @@ return array( 'Project cloned successfully.' => 'Projekt wurde dupliziert.', 'Unable to clone this project.' => 'Duplizieren dieses Projekts schlug fehl.', 'Enable email notifications' => 'E-Mail-Benachrichtigungen einschalten', - 'Task position:' => 'Position der Aufgabe', + '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' => 'Teilaufgabe aktualisiert', - 'Title:' => 'Titel', - 'Status:' => 'Status', + 'Title:' => 'Titel:', + 'Status:' => 'Status:', 'Assignee:' => 'Zuständigkeit:', - 'Time tracking:' => 'Zeittracking', + 'Time tracking:' => 'Zeittracking:', 'New sub-task' => 'Neue Teilaufgabe', 'New attachment added "%s"' => 'Neuer Anhang "%s" wurde hinzugefügt.', 'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s', @@ -356,9 +350,9 @@ return array( 'Remote' => 'Remote', 'Enabled' => 'angeschaltet', 'Disabled' => 'abgeschaltet', - 'Username:' => 'Benutzername', - 'Name:' => 'Name', - 'Email:' => 'E-Mail', + 'Username:' => 'Benutzername:', + 'Name:' => 'Name:', + 'Email:' => 'E-Mail:', 'Notifications:' => 'Benachrichtigungen:', 'Notifications' => 'Benachrichtigungen', 'Account type:' => 'Accounttyp:', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Es sind keine externen Authentisierungsmethoden aktiv.', 'Password modified successfully.' => 'Passwort wurde erfolgreich geändert.', 'Unable to change the password.' => 'Passwort konnte nicht geändert werden.', - 'Change category for the task "%s"' => 'Kategorie der Aufgabe "%s" ändern', 'Change category' => 'Kategorie ändern', '%s updated the task %s' => '%s hat die Aufgabe %s aktualisiert', '%s opened the task %s' => '%s hat die Aufgabe %s geöffnet', @@ -411,13 +404,11 @@ return array( 'Label' => 'Kennzeichnung', 'Database' => 'Datenbank', 'About' => 'Über', - 'Database driver:' => 'Datenbanktreiber', + 'Database driver:' => 'Datenbanktreiber:', 'Board settings' => 'Pinnwandeinstellungen', - 'URL and token' => 'URL und Token', 'Webhook settings' => 'Webhook-Einstellungen', - 'URL for task creation:' => 'URL zur Aufgabenerstellung', 'Reset token' => 'Token zurücksetzen', - 'API endpoint:' => 'API-Endpunkt', + 'API endpoint:' => 'API-Endpunkt:', 'Refresh interval for private board' => 'Aktualisierungsintervall für private Pinnwände', 'Refresh interval for public board' => 'Aktualisierungsintervall für öffentliche Pinnwände', 'Task highlight period' => 'Aufgaben-Hervorhebungsdauer', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Swimlane entfernen', '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.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Alle Swimlanes', 'All colors' => 'Alle Farben', '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', @@ -558,8 +547,8 @@ return array( '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', + 'is a child of' => 'ist ein untergeordnetes Element von', + 'is a parent of' => 'ist ein übergeordnetes Element von', 'targets milestone' => 'betrifft Meilenstein', 'is a milestone of' => 'ist ein Meilenstein von', 'fixes' => 'behebt', @@ -620,9 +609,9 @@ return array( 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.', 'Code' => 'Code', 'Two factor authentication' => 'Zwei-Faktor-Authentifizierung', - 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', + 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI: ', 'Check my code' => 'Überprüfe meinen Code', - 'Secret key: ' => 'Geheimer Schlüssel', + 'Secret key: ' => 'Geheimer Schlüssel: ', 'Test your device' => 'Teste dein Gerät', 'Assign a color when the task is moved to a specific column' => 'Weise eine Farbe zu, wenn die Aufgabe zu einer bestimmten Spalte bewegt wird', '%s via Kanboard' => '%s via Kanboard', @@ -653,15 +642,15 @@ return array( 'Timeframe to calculate new due date' => 'Zeitfenster zur Berechnung für neues Ablaufdatum', 'Base date to calculate new due date' => 'Basisdatum zur Berechnung für neues Ablaufdatum', 'Action date' => 'Aktionsdatum', - 'Base date to calculate new due date: ' => 'Basisdatum zur Berechnung für neues Ablaufdatum:', - 'This task has created this child task: ' => 'Diese Aufgabe hat diese Teilaufgabe erstellt:', + 'Base date to calculate new due date: ' => 'Basisdatum zur Berechnung für neues Ablaufdatum: ', + 'This task has created this child task: ' => 'Diese Aufgabe hat diese Teilaufgabe erstellt: ', 'Day(s)' => 'Tag(e)', 'Existing due date' => 'Existierendes Ablaufdatum', - 'Factor to calculate new due date: ' => 'Faktor zur Berechnung für neues Ablaufdatum', + 'Factor to calculate new due date: ' => 'Faktor zur Berechnung für neues Ablaufdatum: ', 'Month(s)' => 'Monat(e)', 'Recurrence' => 'Wiederholung', - 'This task has been created by: ' => 'DIese Aufgabe wurde erstellt von:', - 'Recurrent task has been generated:' => 'Wiederkehrende Aufgabe wurde erstellt', + 'This task has been created by: ' => 'DIese Aufgabe wurde erstellt von: ', + 'Recurrent task has been generated:' => 'Wiederkehrende Aufgabe wurde erstellt:', 'Timeframe to calculate new due date: ' => 'Zeitfenster zur Berechnung für neues Ablaufdatum: ', 'Trigger to generate recurrent task: ' => 'Auslöser für wiederkehrende Aufgabe: ', 'When task is closed' => 'Wenn Aufgabe geshlossen wird', @@ -708,8 +697,7 @@ return array( 'Project activities for %s' => 'Projektaktivitäten für %s', 'view the board on Kanboard' => 'Pinnwand in Kanboard anzeigen', 'The task have been moved to the first swimlane' => 'Die Aufgabe wurde in die erste Swimlane verschoben', - 'The task have been moved to another swimlane:' => 'Die Aufgaben wurde in ene andere Swimlane verschoben', - 'Overdue tasks for the project "%s"' => 'Überfällige Aufgaben für das Projekt "%s"', + 'The task have been moved to another swimlane:' => 'Die Aufgaben wurde in eine andere Swimlane verschoben:', 'New title: %s' => 'Neuer Titel: %s', 'The task is not assigned anymore' => 'Die Aufgabe ist nicht mehr zugewiesen', 'New assignee: %s' => 'Neue Zuordnung: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Stoppe Timer', 'Start timer' => 'Starte Timer', 'Add project member' => 'Projektmitglied hinzufügen', - 'Enable notifications' => 'Benachrichtigung aktivieren', 'My activity stream' => 'Aktivitätsstream', 'My calendar' => 'Mein Kalender', 'Search tasks' => 'Suche nach Aufgaben', @@ -777,15 +764,15 @@ return array( 'Average time spent' => 'Durchschnittlicher Zeitverbrauch', 'This chart show the average time spent into each column for the last %d tasks.' => 'Dieses Diagramm zeigt die durchschnittliche Zeit in jeder Spalte der letzten %d Aufgaben.', 'Average Lead and Cycle time' => 'Durchschnittliche Zyklus- und Durchlaufzeit', - 'Average lead time: ' => 'Durchschnittliche Durchlaufzeit:', - 'Average cycle time: ' => 'Durchschnittliche Zykluszeit:', + 'Average lead time: ' => 'Durchschnittliche Durchlaufzeit: ', + 'Average cycle time: ' => 'Durchschnittliche Zykluszeit: ', 'Cycle Time' => 'Zykluszeit', 'Lead Time' => 'Durchlaufzeit', 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Das Diagramm zeigt die durchschnittliche Durchlauf- und Zykluszeit der letzten %d Aufgaben über die Zeit an.', 'Average time into each column' => 'Durchschnittzeit in jeder Spalte', 'Lead and cycle time' => 'Durchlauf- und Zykluszeit', - 'Lead time: ' => 'Durchlaufzeit:', - 'Cycle time: ' => 'Zykluszeit:', + 'Lead time: ' => 'Durchlaufzeit: ', + 'Cycle time: ' => 'Zykluszeit: ', 'Time spent into each column' => 'zeit verbracht in jeder Spalte', 'The lead time is the duration between the task creation and the completion.' => 'Die Durchlaufzeit ist die Dauer zwischen Erstellung und Fertigstellung.', 'The cycle time is the duration between the start date and the completion.' => 'Die Zykluszeit ist die Dauer zwischen Start und Fertigstellung.', @@ -793,7 +780,7 @@ return array( 'Set automatically the start date' => 'Setze Startdatum automatisch', 'Edit Authentication' => 'Authentifizierung bearbeiten', 'Remote user' => 'Remote-Benutzer', - 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Google und Github Accounts', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel: LDAP, Google und Github Accounts', 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Wenn die Box "Verbiete Login-Formular" angeschaltet ist, werden Eingaben in das Login Formular ignoriert.', 'New remote user' => 'Neuer Remote-Benutzer', 'New local user' => 'Neuer lokaler Benutzer', @@ -808,7 +795,7 @@ return array( 'no category' => 'keine Kategorie', 'Current assignee: %s' => 'Aktuelle Zuordnung: %s', 'not assigned' => 'nicht zugeordnet', - 'Author:' => 'Autor', + 'Author:' => 'Autor:', 'contributors' => 'Mitwirkende', 'License:' => 'Lizenz:', 'License' => 'Lizenz', @@ -968,7 +955,7 @@ return array( 'Add group member' => 'Gruppenmitglied hinzufügen', 'Do you really want to remove this group: "%s"?' => 'Wollen Sie die Gruppe "%s" wirklich löschen?', 'There is no user in this group.' => 'Es gibt keinen Benutzer in dieser Gruppe.', - 'Remove this user' => 'Diesen Benutzer löschen', + 'Remove this user' => 'Diesen Benutzer entfernen', 'Permissions' => 'Berechtigungen', 'Allowed Users' => 'Berechtigte Benutzer', 'No user have been allowed specifically.' => 'Keine Benutzer mit ausdrücklicher Berechtigung.', @@ -1022,7 +1009,7 @@ return array( 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Kein Plugin hat eine Projekt-Benachrichtigungsmethode registriert. Sie können individuelle Meldungen in Ihrem Benutzerprofil konfigurieren', 'My dashboard' => 'Mein Dashboard', 'My profile' => 'Mein Profil', - 'Project owner: ' => 'Projekt-Besitzer:', + 'Project owner: ' => 'Projekt-Besitzer: ', 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'Die Projekt-Kennung ist optional und muss alphanumerisch sein, beispielsweise: MYPROJECT.', 'Project owner' => 'Projekt-Besitzer', 'Those dates are useful for the project Gantt chart.' => 'Diese Daten sind nützlich für das Gantt-Diagramm.', @@ -1084,10 +1071,10 @@ return array( 'Task #%d' => 'Aufgabe #%d', 'Date and time format' => 'Datums- und Zeitformat', 'Time format' => 'Zeitformat', - 'Start date: ' => 'Anfangsdatum:', - 'End date: ' => 'Enddatum:', - 'New due date: ' => 'Neues Fälligkeitsdatum', - 'Start date changed: ' => 'Anfangsdatum geändert:', + 'Start date: ' => 'Anfangsdatum: ', + 'End date: ' => 'Enddatum: ', + 'New due date: ' => 'Neues Fälligkeitsdatum: ', + 'Start date changed: ' => 'Anfangsdatum geändert: ', 'Disable private projects' => 'Private Projekte deaktivieren', 'Do you really want to remove this custom filter: "%s"?' => 'Wollen Sie diesen benutzerdefinierten Filter wirklich entfernen: "%s"?', 'Remove a custom filter' => 'Benutzerdefinierten Filter entfernen', @@ -1116,7 +1103,7 @@ return array( 'Column created successfully.' => 'Spalte erfolgreich erstellt.', 'Another column with the same name exists in the project' => 'Es gibt bereits eine Spalte mit demselben Namen im Projekt', 'Default filters' => 'Standard-Filter', - 'Your board doesn\'t have any column!' => 'Es gibt keine Spalten in diesem Projekt!', + 'Your board doesn\'t have any columns!' => 'Es gibt keine Spalten in diesem Projekt!', 'Change column position' => 'Position der Spalte ändern', 'Switch to the project overview' => 'Zur Projektübersicht wechseln', 'User filters' => 'Benutzer-Filter', @@ -1135,22 +1122,98 @@ return array( 'There is no action at the moment.' => 'Es gibt zur Zeit keine Aktionen.', 'Import actions from another project' => 'Aktionen von einem anderen Projekt importieren', 'There is no available project.' => 'Es ist kein Projekt verfügbar.', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', - // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', - // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', - // 'Menu' => '', - // 'Set start date' => '', - // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'Local File' => 'Lokale Datei', + 'Configuration' => 'Konfiguration', + 'PHP version:' => 'PHP Version:', + 'PHP SAPI:' => 'PHP SAPI:', + 'OS version:' => 'OS Version:', + 'Database version:' => 'Datenbank Version:', + 'Browser:' => 'Browser:', + 'Task view' => 'Aufgaben Ansicht', + 'Edit task' => 'Aufgabe bearbeiten', + 'Edit description' => 'Beschreibung bearbeiten', + 'New internal link' => 'Neue interne Verbindung', + 'Display list of keyboard shortcuts' => 'Liste der Tastaturkürzel anzeigen', + 'Menu' => 'Menü', + 'Set start date' => 'Anfangsdatum setzen', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Mein Avatar Bild hochladen', + 'Remove my image' => 'Mein Bild entfernen', + 'The OAuth2 state parameter is invalid' => 'Der OAuth2 Statusparameter ist ungültig', + 'User not found.' => 'Benutzer nicht gefunden', + 'Search in activity stream' => 'Im Aktivitätenstrom suchen', + 'My activities' => 'Meine Aktivitäten', + 'Activity until yesterday' => 'Aktivitäten bis gestern', + 'Activity until today' => 'Aktivitäten bis heute', + 'Search by creator: ' => 'nach Ersteller suchen:', + 'Search by creation date: ' => 'nach Datum suchen:', + 'Search by task status: ' => 'nach Aufgabenstatus suchen:', + 'Search by task title: ' => 'nach Titel suchen:', + 'Activity stream search' => 'Im Aktivitätenstrom suchen', + 'Projects where "%s" is manager' => 'Projekte in denen "%s" Manager ist', + 'Projects where "%s" is member' => 'Projekte in denen "%s" Mitglied ist', + 'Open tasks assigned to "%s"' => 'Offene Aufgaben, die "%s" zugeteilt sind', + 'Closed tasks assigned to "%s"' => 'Geschlossene Aufgaben, die "%s" zugeteilt sind', + 'Assign automatically a color based on a priority' => 'Eine Farbe basierend auf einer Priorität automatisch zuordnen', + 'Overdue tasks for the project(s) "%s"' => 'Überfällige Aufgaben des/der Projekt/e "%s"', + 'Upload files' => 'Dateien hochladen', + 'Installed Plugins' => 'Installierte Plugins', + 'Plugin Directory' => 'Plugin Verzeichnis', + 'Plugin installed successfully.' => 'Plugin erfolgreich installiert.', + 'Plugin updated successfully.' => 'Plugin erfolgreich aktualisiert.', + 'Plugin removed successfully.' => 'Plugin erfolgreich entfernt.', + 'Subtask converted to task successfully.' => 'Teilaufgabe erfolgreich in Aufgabe umgewandelt.', + 'Unable to convert the subtask.' => 'Teilaufgabe kann nicht umgewandelt werden.', + 'Unable to extract plugin archive.' => 'Plugin Archiv kann nicht entpackt werden.', + 'Plugin not found.' => 'Plugin nicht gefunden.', + 'You don\'t have the permission to remove this plugin.' => 'Sie dürfen dieses Plugin nicht entfernen.', + 'Unable to download plugin archive.' => 'Plugin Archiv kann nicht herunter geladen werden.', + 'Unable to write temporary file for plugin.' => 'Temporäre Dateien für das Plugin können nicht geschrieben werden.', + 'Unable to open plugin archive.' => 'Kann das Plugin Archiv nicht öffenen.', + 'There is no file in the plugin archive.' => 'Es gibt keine Datei im Plugin Archiv.', + 'Create tasks in bulk' => 'Viele Aufgaben auf einmal erstellen', + 'Your Kanboard instance is not configured to install plugins from the user interface.' => 'Ihre Kanboard Installation ist nicht dafür konfiguriert, Plugins mit dem Benutzerinterface zu installieren.', + 'There is no plugin available.' => 'Es gibt kein Plugin.', + 'Install' => 'Installieren', + 'Update' => 'Aktualisieren', + 'Up to date' => 'Aktuell', + 'Not available' => 'Nicht verfügbar', + 'Remove plugin' => 'Plugin entfernen', + 'Do you really want to remove this plugin: "%s"?' => 'Wollen Sie das Plugin "%s" wirklich entfernen?', + 'Uninstall' => 'Deinstallieren', + 'Listing' => 'Auflistung', + 'Metadata' => 'Metadaten', + 'Manage projects' => 'Projekte verwalten', + 'Convert to task' => 'In Aufgabe umwandeln', + 'Convert sub-task to task' => 'Teilaufgabe in Aufgabe umwandeln', + 'Do you really want to convert this sub-task to a task?' => 'Wollen Sie diese Teilaufgabe wirklich in eine Aufgabe umwandeln?', + 'My task title' => 'Mein Aufgabentitel', + 'Enter one task by line.' => 'Geben Sie eine Aufgabe pro Zeile ein.', + 'Number of failed login:' => 'Anzahl fehlgeschlagener Anmeldungen:', + 'Account locked until:' => 'Konto gesperrt bis:', + 'Email settings' => 'E-Mail Einstellungen', + 'Email sender address' => 'E-Mail Absender Adresse', + 'Email transport' => 'E-Mail Verkehr', + 'Webhook token' => 'Webhook Token', + 'Imports' => 'Importe', + 'Project tags management' => 'Projektbezogenes Schlagwort-Management', + 'Tag created successfully.' => 'Schlagwort erfolgreich erstellt.', + 'Unable to create this tag.' => 'Das Schlagwort kann nicht erstellt werden.', + 'Tag updated successfully.' => 'Schlagwort erfolgreich aktualisiert.', + 'Unable to update this tag.' => 'Das Schlagwort kann nicht aktualisiert werden.', + 'Tag removed successfully.' => 'Schlagwort erfolgreich entfernt.', + 'Unable to remove this tag.' => 'Das Schlagwort kann nicht entfernt werden.', + 'Global tags management' => 'Globales Schlagwort-Management', + 'Tags' => 'Schlagworte', + 'Tags management' => 'Schlagwort-Management', + 'Add new tag' => 'Neues Schlagwort hinzufügen', + 'Edit a tag' => 'Schlagwort bearbeiten', + 'Project tags' => 'Projektbezogene Schlagwörter', + 'There is no specific tag for this project at the moment.' => 'Es gibt zur Zeit kein spezifisches Schlagwort.', + 'Tag' => 'Schlagwort', + 'Remove a tag' => 'Schlagwort entfernen', + 'Do you really want to remove this tag: "%s"?' => 'Soll dieses Schlagwort wirklich entfernt werden: "%s"?', + 'Global tags' => 'Globale Schlagwörter', + 'There is no global tag at the moment.' => 'Es gibt zur Zeit kein globales Schlagwort', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/el_GR/translations.php b/sources/app/Locale/el_GR/translations.php index 9a31e48..d4c5504 100644 --- a/sources/app/Locale/el_GR/translations.php +++ b/sources/app/Locale/el_GR/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Προβολή της εργασίας', 'Remove user' => 'Αφαίρεση χρήστη', 'Do you really want to remove this user: "%s"?' => 'Θέλετε σίγουρα να αφαιρέσετε αυτό τον χρήστη: « %s » ?', - 'New user' => 'Νέος Χρήστης', 'All users' => 'Όλοι οι χρήστες', 'Username' => 'Όνομα χρήστη', 'Password' => 'Κωδικός', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d κλειστές εργασίες', 'No task for this project' => 'Αριθμός εργασιών για το έργο', 'Public link' => 'Δημόσιος σύνδεσμος', - 'Change assignee' => 'Αλλαγή ανάθεσης', - 'Change assignee for the task "%s"' => 'Αλλαγή ανάθεσης για την εργασία « %s »', 'Timezone' => 'Timezone', 'Sorry, I didn\'t find this information in my database!' => 'Δυστυχώς δεν βρέθηκε αυτή η πληροφορία στη βάση δεδομένων', 'Page not found' => 'Η σελίδα δεν βρέθηκε', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Αφήστε μια περιγραφή', 'Comment added successfully.' => 'Το σχόλιο σας προστέθηκε με επιτυχία.', 'Unable to create your comment.' => 'Δεν είναι δυνατή η προσθήκη του σχολίου σας.', - 'Edit this task' => 'Διόρθωση εργασίας', 'Due Date' => 'Μέχρι την ημερομηνία', 'Invalid date' => 'Μη ορθή ημερομηνία', 'Automatic actions' => 'Αυτόματες ενέργειες', @@ -250,7 +246,6 @@ return array( 'Category' => 'Κατηγορία', 'Category:' => 'Κατηγορία:', 'Categories' => 'Κατηγορίες', - 'Category not found.' => 'Η κατηγορία δεν βρέθηκε', 'Your category have been created successfully.' => 'Η κατηγορία δημιουργήθηκε.', 'Unable to create your category.' => 'Δεν είναι δυνατή η δημιουργία της κατηγορίας.', 'Your category have been updated successfully.' => 'Η ενημέρωση της κατηγορίας καταχωρήθηκε με επιτυχία.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Αφαίρεση του αρχείου: « %s » ?', 'Attachments' => 'Συνημμένα', 'Edit the task' => 'Διόρθωση εργασίας', - 'Edit the description' => 'Διόρθωση περιγραφής', 'Add a comment' => 'Προσθήκη σχολίου', 'Edit a comment' => 'Διόρθωση σχολίου', 'Summary' => 'Περίληψη', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Καμία εξωτερική πιστοποιήση ενεργοποιημένη.', 'Password modified successfully.' => 'Το password τροποποιήθηκε με επιτυχία.', 'Unable to change the password.' => 'Αδύνατο να αλλάξει το password.', - 'Change category for the task "%s"' => 'Αλλαγή κατηγορίας της εργασίας « %s »', 'Change category' => 'Αλλαγή κατηγορίας', '%s updated the task %s' => '%s ενημέρωσε την εργασία %s', '%s opened the task %s' => '%s άνοιξε την εργασία %s', @@ -413,9 +406,7 @@ return array( 'About' => 'About', 'Database driver:' => 'Database driver:', 'Board settings' => 'Board settings', - 'URL and token' => 'URL / token', 'Webhook settings' => 'Webhook settings', - 'URL for task creation:' => 'URL για δημιουργία εργασίας:', 'Reset token' => 'Reset token', 'API endpoint:' => 'URL API:', 'Refresh interval for private board' => 'Ανανέωση interval στο private board', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Αφαίρεση λωρίδας', 'Show default swimlane' => 'Εμφάνιση προεπιλεγμένων λωρίδων', 'Swimlane modification for the project "%s"' => 'Τροποποίηση λωρίδας για το έργο « %s »', - 'Swimlane not found.' => 'Η λωρίδα δεν βρέθηκε.', 'Swimlane removed successfully.' => 'Η λωρίδα αφαιρέθηκε με επιτυχία.', 'Swimlanes' => 'Λωρίδες', 'Swimlane updated successfully.' => 'Η λωρίδα ενημερώθηκε με επιτυχία.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Όλες οι λωρίδες', 'All colors' => 'Όλα τα χρώματα', 'Moved to column %s' => 'Μεταφορά στη στήλη %s', - 'Change description' => 'Επεξεργασία περιγραφής', 'User dashboard' => 'Κεντρικό ταμπλό χρήστη', 'Allow only one subtask in progress at the same time for a user' => 'Αφήστε μόνο μία υπο-εργασία σε εξέλιξη ταυτόχρονα για έναν χρήστη', 'Edit column "%s"' => 'Επεξεργασία στήλης « %s »', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'δείτε τον πίνακα στο Kanboard', 'The task have been moved to the first swimlane' => 'Η εργασία αυτή έχει μετακινηθεί στην πρώτη λωρίδα', 'The task have been moved to another swimlane:' => 'Η εργασία αυτή έχει μετακινηθεί σε άλλη λωρίδα:', - 'Overdue tasks for the project "%s"' => 'Εκπρόθεσμες εργασίες για το έργο « %s »', 'New title: %s' => 'Νέος τίτλος: %s', 'The task is not assigned anymore' => 'Η εργασία δεν έχει ανατεθεί πλέον', 'New assignee: %s' => 'Καινούργια ανάθεση: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Διακοπή ρολογιού', 'Start timer' => 'Έναρξη ρολογιού', 'Add project member' => 'Προσθήκη νέου μέλους έργου', - 'Enable notifications' => 'Ενεργοποίηση ειδοποιήσεων', 'My activity stream' => 'Η ροή δραστηριοτήτων μου', 'My calendar' => 'Το ημερολόγιο μου', 'Search tasks' => 'Αναζήτηση εργασιών', @@ -1116,7 +1103,7 @@ return array( 'Column created successfully.' => 'Η στήλη δημιουργήθηκε με επιτυχία.', 'Another column with the same name exists in the project' => 'Μια άλλη στήλη με το ίδιο όνομα υπάρχει στο έργο', 'Default filters' => 'Εξ\' ορισμού φίλτρα', - 'Your board doesn\'t have any column!' => 'Το ταμπλό δεν έχει καμία στήλη!!=', + 'Your board doesn\'t have any columns!' => 'Το ταμπλό δεν έχει καμία στήλη!!=', 'Change column position' => 'Αλλαγή θέσης στήλης', 'Switch to the project overview' => 'Αλλαγή προβολής σε επισκόπηση έργου', 'User filters' => 'Φίλτρα οριζόμενα από τον χρήστη', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/es_ES/translations.php b/sources/app/Locale/es_ES/translations.php index c362336..40c9a8c 100644 --- a/sources/app/Locale/es_ES/translations.php +++ b/sources/app/Locale/es_ES/translations.php @@ -6,8 +6,8 @@ return array( 'None' => 'Ninguno', 'edit' => 'modificar', 'Edit' => 'Modificar', - 'remove' => 'suprimir', - 'Remove' => 'Suprimir', + 'remove' => 'eliminar', + 'Remove' => 'Eliminar', 'Yes' => 'Sí', 'No' => 'No', 'cancel' => 'cancelar', @@ -15,27 +15,26 @@ return array( 'Yellow' => 'Amarillo', 'Blue' => 'Azul', 'Green' => 'Verde', - 'Purple' => 'Púrpura', + 'Purple' => 'Morado', 'Red' => 'Rojo', 'Orange' => 'Naranja', 'Grey' => 'Gris', 'Brown' => 'Marrón', - 'Deep Orange' => 'Naranja Oscuro', - 'Dark Grey' => 'Gris Oscuro', + 'Deep Orange' => 'Naranja oscuro', + 'Dark Grey' => 'Gris oscuro', 'Pink' => 'Rosa', - 'Teal' => 'Verde Azulado', - 'Cyan' => 'Cián', + 'Teal' => 'Verde azulado', + 'Cyan' => 'Cian', 'Lime' => 'Lima', - 'Light Green' => 'Verde Claro', + 'Light Green' => 'Verde claro', 'Amber' => 'Ámbar', 'Save' => 'Guardar', - 'Login' => 'Iniciar sesión (Ingresar)', - 'Official website:' => 'Página web oficial :', + 'Login' => 'Iniciar sesión (ingresar)', + 'Official website:' => 'Página web oficial:', 'Unassigned' => 'No asignado', 'View this task' => 'Ver esta tarea', 'Remove user' => 'Eliminar un usuario', - 'Do you really want to remove this user: "%s"?' => '¿De verdad que desea suprimir a este usuario: « %s » ?', - 'New user' => 'Añadir un usuario', + 'Do you really want to remove this user: "%s"?' => '¿Realmente desea eliminar este usuario: «%s»?', 'All users' => 'Todos los usuarios', 'Username' => 'Nombre de usuario', 'Password' => 'Contraseña', @@ -45,10 +44,10 @@ return array( 'No user' => 'Ningún usuario', 'Forbidden' => 'Acceso denegado', 'Access Forbidden' => 'Acceso denegado', - 'Edit user' => 'Editar un usuario', + 'Edit user' => 'Modificar el usuario', 'Logout' => 'Salir', 'Bad username or password' => 'Usuario o contraseña incorrecto', - 'Edit project' => 'Editar el proyecto', + 'Edit project' => 'Modificar el proyecto', 'Name' => 'Nombre', 'Projects' => 'Proyectos', 'No project' => 'Ningún proyecto', @@ -62,44 +61,44 @@ return array( '%d tasks on the board' => '%d tareas en el tablero', '%d tasks in total' => '%d tareas en total', 'Unable to update this board.' => 'No se puede actualizar este tablero.', - 'Edit board' => 'Editar este tablero', + 'Edit board' => 'Modificar este tablero', 'Disable' => 'Desactivar', 'Enable' => 'Activar', 'New project' => 'Nuevo proyecto', - 'Do you really want to remove this project: "%s"?' => '¿De verdad que desea eliminar este proyecto: « %s » ?', - 'Remove project' => 'Suprimir el proyecto', - 'Edit the board for "%s"' => 'Modificar el tablero para « %s »', + 'Do you really want to remove this project: "%s"?' => '¿Realmente desea eliminar este proyecto: «%s»?', + 'Remove project' => 'Eliminar el proyecto', + 'Edit the board for "%s"' => 'Modificar el tablero para «%s»', 'All projects' => 'Todos los proyectos', 'Add a new column' => 'Añadir una nueva columna', 'Title' => 'Título', 'Assigned to %s' => 'Asignada a %s', - 'Remove a column' => 'Suprimir esta columna', - 'Remove a column from a board' => 'Suprimir una columna de un tablero', - 'Unable to remove this column.' => 'No se puede suprimir esta columna.', - 'Do you really want to remove this column: "%s"?' => '¿De vedad que desea eliminar esta columna : « %s » ?', - 'This action will REMOVE ALL TASKS associated to this column!' => '¡Esta acción SUPRIMIRÁ TODAS LAS TAREAS asociadas a esta columna!', + 'Remove a column' => 'Eliminar esta columna', + 'Remove a column from a board' => 'Eliminar una columna de un tablero', + 'Unable to remove this column.' => 'No se puede eliminar esta columna.', + 'Do you really want to remove this column: "%s"?' => '¿De vedad que desea eliminar esta columna: «%s»?', + 'This action will REMOVE ALL TASKS associated to this column!' => '¡Esta acción ELIMINARÁ TODAS LAS TAREAS asociadas a esta columna!', 'Settings' => 'Preferencias', - 'Application settings' => 'Parámetros de la aplicación', + 'Application settings' => 'Preferencias de la aplicación', 'Language' => 'Idioma', - 'Webhook token:' => 'Ficha de seguridad (token) para los disparadores Web (webhooks):', - 'API token:' => 'Ficha de seguridad (token) para API:', + 'Webhook token:' => 'Token de los disparadores web (webhooks):', + 'API token:' => 'Token de la API:', 'Database size:' => 'Tamaño de la base de datos:', 'Download the database' => 'Descargar la base de datos', 'Optimize the database' => 'Optimizar la base de datos', - '(VACUUM command)' => '(Comando VACUUM)', - '(Gzip compressed Sqlite file)' => '(Archivo Sqlite comprimido en Gzip)', + '(VACUUM command)' => '(comando VACUUM)', + '(Gzip compressed Sqlite file)' => '(archivo Sqlite comprimido en Gzip)', 'Close a task' => 'Cerrar una tarea', - 'Edit a task' => 'Editar una tarea', + 'Edit a task' => 'Modificar una tarea', 'Column' => 'Columna', 'Color' => 'Color', - 'Assignee' => 'Concesionario', - 'Create another task' => 'Crear una nueva tarea', + 'Assignee' => 'Responsable', + 'Create another task' => 'Crear otra tarea', 'New task' => 'Nueva tarea', 'Open a task' => 'Abrir una tarea', - 'Do you really want to open this task: "%s"?' => '¿Realmente desea abrir esta tarea: « %s » ?', + 'Do you really want to open this task: "%s"?' => '¿Realmente desea abrir esta tarea: «%s»?', 'Back to the board' => 'Volver al tablero', 'There is nobody assigned' => 'No hay nadie asignado a esta tarea', - 'Column on the board:' => 'Columna en el tablero: ', + 'Column on the board:' => 'Columna en el tablero:', 'Close this task' => 'Cerrar esta tarea', 'Open this task' => 'Abrir esta tarea', 'There is no description.' => 'No hay descripción.', @@ -118,15 +117,15 @@ return array( 'The project id is required' => 'El identificador del proyecto es obligatorio', 'The project name is required' => 'El nombre del proyecto es obligatorio', 'The title is required' => 'El título es obligatorio', - 'Settings saved successfully.' => 'Parámetros guardados correctamente.', - 'Unable to save your settings.' => 'No se pueden guardar sus parámetros.', + 'Settings saved successfully.' => 'Preferencias guardadas correctamente.', + 'Unable to save your settings.' => 'No se pueden guardar sus preferencias.', 'Database optimization done.' => 'Optimización de la base de datos terminada.', 'Your project have been created successfully.' => 'El proyecto ha sido creado correctamente.', 'Unable to create your project.' => 'No se puede crear el proyecto.', 'Project updated successfully.' => 'El proyecto ha sido actualizado correctamente.', 'Unable to update this project.' => 'No se puede actualizar el proyecto.', - 'Unable to remove this project.' => 'No se puede suprimir este proyecto.', - 'Project removed successfully.' => 'El proyecto ha sido borrado correctamente.', + 'Unable to remove this project.' => 'No se puede eliminar este proyecto.', + 'Project removed successfully.' => 'El proyecto ha sido eliminado correctamente.', 'Project activated successfully.' => 'El proyecto ha sido activado correctamente.', 'Unable to activate this project.' => 'No se puede activar el proyecto.', 'Project disabled successfully.' => 'El proyecto ha sido desactivado correctamente.', @@ -135,7 +134,7 @@ return array( 'Task opened successfully.' => 'La tarea ha sido abierta correctamente.', 'Unable to close this task.' => 'No se puede cerrar esta tarea.', 'Task closed successfully.' => 'La tarea ha sido cerrada correctamente.', - 'Unable to update your task.' => 'No se puede modificar esta tarea.', + 'Unable to update your task.' => 'No se puede actualizar esta tarea.', 'Task updated successfully.' => 'La tarea ha sido actualizada correctamente.', 'Unable to create your task.' => 'No se puede crear esta tarea.', 'Task created successfully.' => 'La tarea ha sido creada correctamente.', @@ -146,7 +145,7 @@ return array( 'User removed successfully.' => 'El usuario ha sido creado correctamente.', 'Unable to remove this user.' => 'No se puede crear este usuario.', 'Board updated successfully.' => 'El tablero ha sido actualizado correctamente.', - 'Ready' => 'Listo', + 'Ready' => 'Preparado', 'Backlog' => 'En espera', 'Work in progress' => 'En curso', 'Done' => 'Hecho', @@ -154,11 +153,9 @@ return array( 'Id' => 'Identificador', '%d closed tasks' => '%d tareas completadas', 'No task for this project' => 'Ninguna tarea para este proyecto', - 'Public link' => 'Vinculación pública', - 'Change assignee' => 'Cambiar concesionario', - 'Change assignee for the task "%s"' => 'Cambiar el concesionario para la tarea « %s »', + 'Public link' => 'Enlace público', 'Timezone' => 'Zona horaria', - 'Sorry, I didn\'t find this information in my database!' => '¡Lo siento no he encontrado esta información en mi base de datos!', + 'Sorry, I didn\'t find this information in my database!' => '¡Lo siento, no he encontrado esta información en mi base de datos!', 'Page not found' => 'Página no encontrada', 'Complexity' => 'Complejidad', 'Task limit' => 'Número máximo de tareas', @@ -170,17 +167,16 @@ return array( 'Leave a description' => 'Dejar una descripción', 'Comment added successfully.' => 'El comentario ha sido añadido correctamente.', 'Unable to create your comment.' => 'No se puede crear este comentario.', - 'Edit this task' => 'Editar esta tarea', 'Due Date' => 'Fecha límite', 'Invalid date' => 'Fecha no válida', '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.', - 'Remove an action' => 'Suprimir una acción', - 'Unable to remove this action.' => 'No se puede suprimir esta accción.', - 'Action removed successfully.' => 'La acción ha sido borrada correctamente.', - 'Automatic actions for the project "%s"' => 'Acciones automatizadas para este proyecto « %s »', - 'Add an action' => 'Agregar una acción', + 'Remove an action' => 'Eliminar una acción', + 'Unable to remove this action.' => 'No se puede eliminar esta acción.', + 'Action removed successfully.' => 'La acción ha sido eliminada correctamente.', + 'Automatic actions for the project "%s"' => 'Acciones automatizadas para el proyecto «%s»', + 'Add an action' => 'Añadir una acción', 'Event name' => 'Nombre del evento', 'Action name' => 'Nombre de la acción', 'Action parameters' => 'Parámetros de la acción', @@ -188,9 +184,9 @@ return array( 'Event' => 'Evento', 'When the selected event occurs execute the corresponding action.' => 'Cuando tiene lugar el evento seleccionado, ejecutar la acción correspondiente.', 'Next step' => 'Etapa siguiente', - 'Define action parameters' => 'Definición de los parametros de la acción', - 'Do you really want to remove this action: "%s"?' => '¿Realmente desea suprimir esta acción « %s » ?', - 'Remove an automatic action' => 'Suprimir una acción automatizada', + 'Define action parameters' => 'Define los parámetros de la acción', + 'Do you really want to remove this action: "%s"?' => '¿Realmente desea eliminar esta acción «%s»?', + 'Remove an automatic action' => 'Eliminar una acción automatizada', 'Assign the task to a specific user' => 'Asignar una tarea a un usuario especifico', 'Assign the task to the person who does the action' => 'Asignar la tarea al usuario que hace la acción', 'Duplicate the task to another project' => 'Duplicar la tarea a otro proyecto', @@ -203,22 +199,22 @@ return array( 'Position' => 'Posición', 'Duplicate to another project' => 'Duplicar a otro proyecto', 'Duplicate' => 'Duplicar', - 'link' => 'vinculación', + 'link' => 'enlace', 'Comment updated successfully.' => 'El comentario ha sido actualizado correctamente.', 'Unable to update your comment.' => 'No se puede actualizar este comentario.', - 'Remove a comment' => 'Suprimir un comentario', - 'Comment removed successfully.' => 'El comentario ha sido suprimido correctamente.', - 'Unable to remove this comment.' => 'No se puede suprimir este comentario.', - 'Do you really want to remove this comment?' => '¿Desea suprimir este comentario?', - 'Current password for the user "%s"' => 'Contraseña actual para el usuario: « %s »', + 'Remove a comment' => 'Eliminar un comentario', + 'Comment removed successfully.' => 'El comentario ha sido eliminado correctamente.', + 'Unable to remove this comment.' => 'No se puede eliminar este comentario.', + 'Do you really want to remove this comment?' => '¿Realmente desea eliminar este comentario?', + 'Current password for the user "%s"' => 'Contraseña actual para el usuario: «%s»', 'The current password is required' => 'La contraseña es obligatoria', - 'Wrong password' => 'contraseña incorrecta', + 'Wrong password' => 'Contraseña incorrecta', 'Unknown' => 'Desconocido', 'Last logins' => 'Últimos ingresos', 'Login date' => 'Fecha de ingreso', 'Authentication method' => 'Método de autenticación', 'IP address' => 'Dirección IP', - 'User agent' => 'Agente de usuario', + 'User agent' => 'Agente de usuario (User agent)', 'Persistent connections' => 'Conexión persistente', 'No session.' => 'No existe sesión.', 'Expiration date' => 'Fecha de expiración', @@ -228,7 +224,7 @@ return array( 'Open' => 'Abierto', 'Closed' => 'Cerrado', 'Search' => 'Buscar', - 'Nothing found.' => 'Nada hallado.', + 'Nothing found.' => 'No se ha encontrado nada.', 'Due date' => 'Fecha límite', 'Others formats accepted: %s and %s' => 'Otros formatos aceptados: %s y %s', 'Description' => 'Descripción', @@ -236,98 +232,96 @@ return array( '%d comment' => '%d comentario', 'Email address invalid' => 'Dirección de correo inválida', 'Your external account is not linked anymore to your profile.' => 'Su cuenta externa se ha desvinculado de su perfil.', - 'Unable to unlink your external account.' => 'Imposible desvincular su cuenta externa.', + 'Unable to unlink your external account.' => 'No se puede desvincular su cuenta externa.', 'External authentication failed' => 'Falló la autenticación externa', - 'Your external account is linked to your profile successfully.' => 'Su cuenta externa se ha vinculado exitosamente con su perfil.', - 'Email' => 'Correo', - 'Task removed successfully.' => 'Tarea suprimida correctamente.', - 'Unable to remove this task.' => 'No pude suprimir esta tarea.', - 'Remove a task' => 'Borrar una tarea', - 'Do you really want to remove this task: "%s"?' => '¿De verdad que quieres suprimir esta tarea: "%s"?', + 'Your external account is linked to your profile successfully.' => 'Su cuenta externa se ha vinculado correctamente con su perfil.', + 'Email' => 'Correo electrónico', + 'Task removed successfully.' => 'Tarea eliminada correctamente.', + 'Unable to remove this task.' => 'No se puede eliminar esta tarea.', + 'Remove a task' => 'Eliminar una tarea', + 'Do you really want to remove this task: "%s"?' => '¿Realmente desea eliminar esta tarea: «%s»?', 'Assign automatically a color based on a category' => 'Asignar un color de forma automática basándose en la categoría', 'Assign automatically a category based on a color' => 'Asignar una categoría de forma automática basándose en el color', 'Task creation or modification' => 'Creación o Edición de Tarea', 'Category' => 'Categoría', 'Category:' => 'Categoría:', 'Categories' => 'Categorías', - 'Category not found.' => 'Categoría no hallada.', 'Your category have been created successfully.' => 'Se ha creado su categoría correctamente.', - 'Unable to create your category.' => 'No pude crear su categoría.', + 'Unable to create your category.' => 'No se puede crear su categoría.', 'Your category have been updated successfully.' => 'Se ha actualizado su categoría correctamente.', - 'Unable to update your category.' => 'No pude actualizar su categoría.', - 'Remove a category' => 'Suprimir una categoría', - 'Category removed successfully.' => 'Categoría suprimida correctamente.', - 'Unable to remove this category.' => 'No pude suprimir esta categoría.', - 'Category modification for the project "%s"' => 'Modificación de categoría pra el proyecto "%s"', - 'Category Name' => 'Nombre de Categoría', + 'Unable to update your category.' => 'No se puede actualizar su categoría.', + 'Remove a category' => 'Eliminar una categoría', + 'Category removed successfully.' => 'Categoría eliminada correctamente.', + 'Unable to remove this category.' => 'No se puede eliminar esta categoría.', + 'Category modification for the project "%s"' => 'Modificación de la categoría para el proyecto «%s»', + 'Category Name' => 'Nombre de la categoría', 'Add a new category' => 'Añadir una nueva categoría', - 'Do you really want to remove this category: "%s"?' => '¿De verdad que quieres suprimir esta categoría: "%s"?', + 'Do you really want to remove this category: "%s"?' => '¿Realmente desea eliminar esta categoría: «%s»?', 'All categories' => 'Todas las categorías', 'No category' => 'Sin categoría', 'The name is required' => 'El nombre es obligatorio', - 'Remove a file' => 'Borrar un fichero', - 'Unable to remove this file.' => 'No pude borrar este fichero.', - 'File removed successfully.' => 'Fichero borrado correctamente.', + 'Remove a file' => 'Eliminar un fichero', + 'Unable to remove this file.' => 'No se puede eliminar este fichero.', + 'File removed successfully.' => 'Fichero eliminado correctamente.', 'Attach a document' => 'Adjuntar un documento', - 'Do you really want to remove this file: "%s"?' => '¿De verdad que quiere borrar este fichero: "%s"?', + 'Do you really want to remove this file: "%s"?' => '¿Realmente desea eliminar este fichero: «%s»?', 'Attachments' => 'Adjuntos', - 'Edit the task' => 'Editar la tarea', - 'Edit the description' => 'Editar la descripción', + 'Edit the task' => 'Modificar la tarea', 'Add a comment' => 'Añadir un comentario', - 'Edit a comment' => 'Editar un comentario', + 'Edit a comment' => 'Modificar un comentario', 'Summary' => 'Resumen', 'Time tracking' => 'Seguimiento temporal', 'Estimate:' => 'Estimado:', 'Spent:' => 'Transcurrido:', - 'Do you really want to remove this sub-task?' => '¿De verdad que quiere suprimir esta sub-tarea?', + 'Do you really want to remove this sub-task?' => '¿Realmente desea eliminar esta subtarea?', 'Remaining:' => 'Restante:', 'hours' => 'horas', 'spent' => 'transcurrido', 'estimated' => 'estimado', - 'Sub-Tasks' => 'Sub-Tareas', - 'Add a sub-task' => 'Añadir una sub-tarea', - 'Original estimate' => 'Estimado Original', - 'Create another sub-task' => 'Crear otra sub-tarea', - 'Time spent' => 'Tiempo Transcurrido', - 'Edit a sub-task' => 'Editar una sub-tarea', - 'Remove a sub-task' => 'Suprimir una sub-tarea', + 'Sub-Tasks' => 'Subtareas', + 'Add a sub-task' => 'Añadir una subtarea', + 'Original estimate' => 'Estimación original', + 'Create another sub-task' => 'Crear otra subtarea', + 'Time spent' => 'Tiempo transcurrido', + 'Edit a sub-task' => 'Modificar una subtarea', + 'Remove a sub-task' => 'Eliminar una subtarea', 'The time must be a numeric value' => 'El tiempo debe de ser un valor numérico', 'Todo' => 'Por hacer', 'In progress' => 'En progreso', - 'Sub-task removed successfully.' => 'Sub-tarea suprimida correctamente.', - 'Unable to remove this sub-task.' => 'No pude suprimir esta sub-tarea.', - 'Sub-task updated successfully.' => 'Sub-tarea actualizada correctamente.', - 'Unable to update your sub-task.' => 'No pude actualizar su sub-tarea.', - 'Unable to create your sub-task.' => 'No pude crear su sub-tarea.', - 'Sub-task added successfully.' => 'Sub-tarea añadida correctamente.', - 'Maximum size: ' => 'Tamaño máximo', - 'Unable to upload the file.' => 'No pude cargar el fichero.', + 'Sub-task removed successfully.' => 'Subtarea eliminada correctamente.', + 'Unable to remove this sub-task.' => 'No se puede eliminar esta subtarea.', + 'Sub-task updated successfully.' => 'Subtarea actualizada correctamente.', + 'Unable to update your sub-task.' => 'No se puede actualizar esta subtarea.', + 'Unable to create your sub-task.' => 'No se puede crear la subtarea.', + 'Sub-task added successfully.' => 'Subtarea añadida correctamente.', + 'Maximum size: ' => 'Tamaño máximo: ', + 'Unable to upload the file.' => 'No se puede cargar el fichero.', 'Display another project' => 'Mostrar otro proyecto', 'Created by %s' => 'Creado por %s', 'Tasks Export' => 'Exportar tareas', - 'Tasks exportation for "%s"' => 'Exportación de tareas para "%s"', + 'Tasks exportation for "%s"' => 'Exportación de tareas para «%s»', 'Start Date' => 'Fecha de inicio', 'End Date' => 'Fecha final', 'Execute' => 'Ejecutar', - 'Task Id' => 'ID de tarea', + 'Task Id' => 'Identificador de tarea', 'Creator' => 'Creador', 'Modification date' => 'Fecha de modificación', - 'Completion date' => 'Fecha de terminación', + 'Completion date' => 'Fecha de finalización', 'Clone' => 'Clonar', - 'Project cloned successfully.' => 'Proyecto clonado correctamente', - 'Unable to clone this project.' => 'Impsible clonar proyecto', + 'Project cloned successfully.' => 'Proyecto clonado correctamente.', + 'Unable to clone this project.' => 'No se puede clonar este proyecto.', 'Enable email notifications' => 'Habilitar notificaciones por correo electrónico', - 'Task position:' => 'Posición de la tarea', - 'The task #%d have been opened.' => 'La tarea #%d ha sido abierta', - 'The task #%d have been closed.' => 'La tarea #%d ha sido cerrada', + 'Task position:' => 'Posición de la tarea:', + 'The task #%d have been opened.' => 'La tarea #%d ha sido abierta.', + 'The task #%d have been closed.' => 'La tarea #%d ha sido cerrada.', 'Sub-task updated' => 'Subtarea actualizada', 'Title:' => 'Título:', 'Status:' => 'Estado:', - 'Assignee:' => 'Concesionario:', + 'Assignee:' => 'Responsable:', 'Time tracking:' => 'Control de tiempo:', 'New sub-task' => 'Nueva subtarea', - 'New attachment added "%s"' => 'Nuevo adjunto agregado "%s"', - 'New comment posted by %s' => 'Nuevo comentario agregado por %s', + 'New attachment added "%s"' => 'Nuevo adjunto añadido «%s»', + 'New comment posted by %s' => 'Nuevo comentario añadido por %s', 'New attachment' => 'Nuevo adjunto', 'New comment' => 'Nuevo comentario', 'Comment updated' => 'Comentario actualizado', @@ -343,39 +337,38 @@ return array( 'Disable public access' => 'Desactivar acceso público', 'Enable public access' => 'Activar acceso público', 'Public access disabled' => 'Acceso público desactivado', - 'Do you really want to disable this project: "%s"?' => '¿Realmente desea desactivar este proyecto: "%s"?', - 'Do you really want to enable this project: "%s"?' => '¿Realmente desea activar este proyecto: "%s"?', - 'Project activation' => 'Activación de Proyecto', + 'Do you really want to disable this project: "%s"?' => '¿Realmente desea desactivar este proyecto: «%s»?', + 'Do you really want to enable this project: "%s"?' => '¿Realmente desea activar este proyecto: «%s»?', + 'Project activation' => 'Activación de proyecto', 'Move the task to another project' => 'Mover la tarea a otro proyecto', 'Move to another project' => 'Mover a otro proyecto', 'Do you really want to duplicate this task?' => '¿Realmente desea duplicar esta tarea?', 'Duplicate a task' => 'Duplicar una tarea', 'External accounts' => 'Cuentas externas', - 'Account type' => 'Tipo de Cuenta', + 'Account type' => 'Tipo de cuenta', 'Local' => 'Local', 'Remote' => 'Remota', 'Enabled' => 'Activada', 'Disabled' => 'Desactivada', - 'Username:' => 'Nombre de Usuario:', + 'Username:' => 'Nombre de usuario:', 'Name:' => 'Nombre:', 'Email:' => 'Correo electrónico:', 'Notifications:' => 'Notificaciones:', 'Notifications' => 'Notificaciones', - 'Account type:' => 'Tipo de Cuenta:', - 'Edit profile' => 'Editar perfil', + 'Account type:' => 'Tipo de cuenta:', + 'Edit profile' => 'Modificar perfil', 'Change password' => 'Cambiar contraseña', - 'Password modification' => 'Modificacion de contraseña', + 'Password modification' => 'Modificación de contraseña', 'External authentications' => 'Autenticación externa', 'Never connected.' => 'Nunca se ha conectado.', 'No external authentication enabled.' => 'Sin autenticación externa activa.', 'Password modified successfully.' => 'Contraseña cambiada correctamente.', - 'Unable to change the password.' => 'No pude cambiar la contraseña.', - 'Change category for the task "%s"' => 'Cambiar la categoría de la tarea "%s"', + 'Unable to change the password.' => 'No se puede cambiar la contraseña.', 'Change category' => 'Cambiar categorí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 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', @@ -394,40 +387,38 @@ return array( '%s created the task #%d' => '%s creó la tarea #%d', '%s closed the task #%d' => '%s cerró la tarea #%d', '%s open the task #%d' => '%s abrió la tarea #%d', - '%s moved the task #%d to the column "%s"' => '%s movió la tarea #%d a la columna "%s"', - '%s moved the task #%d to the position %d in the column "%s"' => '%s movió la tarea #%d a la posición %d de la columna "%s"', + '%s moved the task #%d to the column "%s"' => '%s movió la tarea #%d a la columna «%s»', + '%s moved the task #%d to the position %d in the column "%s"' => '%s movió la tarea #%d a la posición %d de la columna «%s»', 'Activity' => 'Actividad', - '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 concesionario de la tarea', - '%s change the assignee of the task #%d to %s' => '%s cambió el concesionario de la tarea #%d por %s', - '%s changed the assignee of the task %s to %s' => '%s cambió el concesionario de la tarea %s por %s', - 'New password for the user "%s"' => 'Nueva contraseña para el usuario "%s"', + 'Default values are "%s"' => 'Los valores por defecto son «%s»', + 'Default columns for new projects (Comma-separated)' => 'Columnas por defecto para los nuevos proyectos (separadas mediante comas)', + 'Task assignee change' => 'Cambiar responsable de la tarea', + '%s change the assignee of the task #%d to %s' => '%s cambió el responsable de la tarea #%d por %s', + '%s changed the assignee of the task %s to %s' => '%s cambió el responsable de la tarea %s por %s', + 'New password for the user "%s"' => 'Nueva contraseña para el usuario «%s»', 'Choose an event' => 'Seleccione un evento', 'Create a task from an external provider' => 'Crear una tarea a partir de un proveedor externo', - 'Change the assignee based on an external username' => 'Cambiar el concesionario basado en un nombre de usuario externo', + 'Change the assignee based on an external username' => 'Cambiar el responsable 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', 'Label' => 'Etiqueta', - 'Database' => 'Base de Datos', + 'Database' => 'Base de datos', 'About' => 'Acerca de', - 'Database driver:' => 'Controlador (Driver) de la base de datos', - 'Board settings' => 'Configuraciones del Tablero', - 'URL and token' => 'URL y ficha', - 'Webhook settings' => 'Configuraciones del Disparador Web (Webhook)', - 'URL for task creation:' => 'URL para la creación de tareas', - 'Reset token' => 'Limpiar ficha', - 'API endpoint:' => 'Punto final del API', + 'Database driver:' => 'Controlador de la base de datos (driver):', + 'Board settings' => 'Preferencias del tablero', + 'Webhook settings' => 'Preferencias del disparador web (webhook)', + 'Reset token' => 'Limpiar token', + 'API endpoint:' => 'Endpoint del API:', 'Refresh interval for private board' => 'Intervalo de refresco del tablero privado', 'Refresh interval for public board' => 'Intervalo de refresco del tablero público', 'Task highlight period' => 'Periodo de 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)', + '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 fue 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', - 'Token regenerated.' => 'Ficha regenerada.', + '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"', + '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', 'Add' => 'Añadir', @@ -439,93 +430,91 @@ return array( 'Dashboard' => 'Tablero', 'Confirmation' => 'Confirmación', 'Allow everybody to access to this project' => 'Permitir a cualquiera acceder a este proyecto', - 'Everybody have access to this project.' => 'Cualquiera tiene acceso a este proyecto', - 'Webhooks' => 'Disparadores Web (Webhooks)', + 'Everybody have access to this project.' => 'Cualquiera tiene acceso a este proyecto.', + 'Webhooks' => 'Disparadores web (webhooks)', 'API' => 'API', 'Create a comment from an external provider' => 'Crear un comentario a partir de un proveedor externo', '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', + 'Your are not member of any project.' => 'No eres miembro de ningún proyecto.', 'Percentage' => 'Porcentaje', 'Number of tasks' => 'Número de tareas', 'Task distribution' => 'Distribución de tareas', 'Reportings' => 'Informes', - 'Task repartition for "%s"' => 'Repartición de tareas para "%s"', + '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"', + 'User repartition for "%s"' => 'Repartición de usuarios para «%s»', 'Clone this project' => 'Clonar este proyecto', - 'Column removed successfully.' => 'Columna eliminada correctamente', + 'Column removed successfully.' => 'Columna eliminada correctamente.', '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 is required' => 'El id de la subtarea es obligatorio', '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 is required' => 'El id de la tarea es obligatorio', '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' => 'Este valor es requerido', + 'This value is required' => 'Este valor es obligatorio', 'This value must be numeric' => 'Este valor debe ser numérico', - 'Unable to create this task.' => 'Imposible crear esta tarea', + 'Unable to create this task.' => 'No se puede 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"', + 'Cumulative flow diagram for "%s"' => 'Diagrama de flujo acumulativo para «%s»', + 'Daily project summary' => 'Resumen diario del proyecto', + 'Daily project summary export' => 'Exportar resumen diario del proyecto', + 'Daily project summary export for "%s"' => 'Exportar resumen diario del proyecto para «%s»', 'Exports' => 'Exportaciones', - 'This export contains the number of tasks per column grouped per day.' => 'Esta exportación contiene el número de tereas por columna agrupada por día.', + 'This export contains the number of tasks per column grouped per day.' => 'Esta exportación contiene el número de tareas por columna agrupadas por día.', 'Active swimlanes' => 'Calles activas', 'Add a new swimlane' => 'Añadir nueva calle', 'Change default swimlane' => 'Cambiar la calle por defecto', 'Default swimlane' => 'Calle por defecto', - 'Do you really want to remove this swimlane: "%s"?' => '¿Realmente quiere quitar esta calle: "%s"?', + 'Do you really want to remove this swimlane: "%s"?' => '¿Realmente desea eliminar esta calle: «%s»?', 'Inactive swimlanes' => 'Calles inactivas', - 'Remove a swimlane' => 'Quitar un calle', + 'Remove a swimlane' => 'Eliminar una calle', 'Show default swimlane' => 'Mostrar calle por defecto', - 'Swimlane modification for the project "%s"' => 'Modificación de la calle para el proyecto "%s"', - 'Swimlane not found.' => 'Calle no encontrada', - 'Swimlane removed successfully.' => 'Caalle eliminada correctamente', + 'Swimlane modification for the project "%s"' => 'Modificación de la calle para el proyecto «%s»', + 'Swimlane removed successfully.' => 'Calle eliminada correctamente.', 'Swimlanes' => 'Calles', - 'Swimlane updated successfully.' => 'Calle actualizada correctamente', - 'The default swimlane have been updated successfully.' => 'La calle por defecto ha sido actualizada correctamente', - 'Unable to remove this swimlane.' => 'Imposible de quitar esta calle', - 'Unable to update this swimlane.' => 'Imposible de actualizar esta calle', - 'Your swimlane have been created successfully.' => 'Su calle ha sido creada correctamente', + 'Swimlane updated successfully.' => 'Calle actualizada correctamente.', + 'The default swimlane have been updated successfully.' => 'La calle por defecto ha sido actualizada correctamente.', + 'Unable to remove this swimlane.' => 'No es posible eliminar esta calle.', + 'Unable to update this swimlane.' => 'No es posible actualizar esta calle.', + 'Your swimlane have been created successfully.' => 'Su calle ha sido creada correctamente.', 'Example: "Bug, Feature Request, Improvement"' => 'Ejemplo: "Error, Solicitud de característica, Mejora', 'Default categories for new projects (Comma-separated)' => 'Categorías por defecto para nuevos proyectos (separadas por comas)', 'Integrations' => 'Integraciones', 'Integration with third-party services' => 'Integración con servicios de terceros', - 'Subtask Id' => 'Id de Subtarea', + 'Subtask Id' => 'Identificador de subtarea', 'Subtasks' => 'Subtareas', - 'Subtasks Export' => 'Exportación de Subtareas', - 'Subtasks exportation for "%s"' => 'Exportación de subtareas para "%s"', + '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', + 'Application default' => 'Predefinido por la aplicación', + 'Language:' => 'Idioma:', + 'Timezone:' => 'Zona horaria:', 'All columns' => 'Todas las columnas', 'Calendar' => 'Calendario', 'Next' => 'Siguiente', '#%d' => '#%d', 'All swimlanes' => 'Todas las calles', 'All colors' => 'Todos los colores', - 'Moved to column %s' => 'Movido a columna %s', - 'Change description' => 'Cambiar descripción', + 'Moved to column %s' => 'Movida a la columna %s', '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', - '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', + 'Edit column "%s"' => 'Modificar la columna %s', + 'Select the new status of the subtask: "%s"' => 'Seleccionar el nuevo estado de la subtarea: «%s»', + 'Subtask timesheet' => 'Hoja temporal de la subtarea', + 'There is nothing to show.' => 'No hay nada que mostrar.', + 'Time Tracking' => 'Seguimiento temporal', + 'You already have one subtask in progress' => 'Ya tiene una subtarea en progreso', 'Which parts of the project do you want to duplicate?' => '¿Qué partes del proyecto desea duplicar?', 'Disallow login form' => 'Deshabilitar formulario de ingreso', 'Start' => 'Inicio', @@ -534,42 +523,42 @@ return array( 'Days in this column' => 'Días en esta columna', '%dd' => '%dd', 'Add a new link' => 'Añadir nuevo enlace', - 'Do you really want to remove this link: "%s"?' => '¿Realmente quiere quitar este enlace: "%s"?', - 'Do you really want to remove this link with task #%d?' => '¿Realmente quiere quitar este enlace con esta tarea: #%d?', - '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', + 'Do you really want to remove this link: "%s"?' => '¿Realmente desea eliminar este enlace: «%s»?', + 'Do you really want to remove this link with task #%d?' => '¿Realmente desea eliminar este enlace con la tarea #%d?', + 'Field required' => 'Campo requerido', + 'Link added successfully.' => 'Enlace añadido correctamente.', + 'Link updated successfully.' => 'Enlace actualizado correctamente.', + 'Link removed successfully.' => 'Enlace eliminado correctamente.', + 'Link labels' => 'Etiquetas de enlace', + 'Link modification' => 'Modificación del enlace', 'Links' => 'Enlaces', 'Link settings' => 'Preferencias de enlace', 'Opposite label' => 'Etiqueta opuesta', - 'Remove a link' => 'Quitar un enlace', + 'Remove a link' => 'Eliminar 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.', + 'There is no link.' => 'No hay enlace.', + 'This label must be unique' => 'Esta etiqueta debe ser única', + 'Unable to create your link.' => 'No se puede crear el enlace.', + 'Unable to update your link.' => 'No se puede actualizar el enlace.', + 'Unable to remove this link.' => 'No se puede eliminar este enlace.', 'relates to' => 'se refiere a', - 'blocks' => 'bloques', - 'is blocked by' => 'bloqueado por', + 'blocks' => 'bloquea', + 'is blocked by' => 'está bloqueada por', 'duplicates' => 'duplica', - 'is duplicated by' => 'está duplicado por', + 'is duplicated by' => 'está duplicada 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', + 'is fixed by' => 'es arreglada por', 'This task' => 'Esta tarea', '<1h' => '<1h', '%dh' => '%dh', - 'Expand tasks' => 'Espande tareas', - 'Collapse tasks' => 'Colapsa tareas', - 'Expand/collapse tasks' => 'Expande/colapasa tareas', + 'Expand tasks' => 'Expandir tareas', + 'Collapse tasks' => 'Colapsar tareas', + 'Expand/collapse tasks' => 'Expande/colapsa tareas', 'Close dialog box' => 'Cerrar caja de diálogo', 'Submit a form' => 'Enviar formulario', 'Board view' => 'Vista de tablero', @@ -589,130 +578,129 @@ return array( 'download' => 'descargar', 'EUR - Euro' => 'EUR - Euro', 'GBP - British Pound' => 'GBP - Libra británica', - 'INR - Indian Rupee' => 'INR - Rupias indúes', + 'INR - Indian Rupee' => 'INR - Rupia india', 'JPY - Japanese Yen' => 'JPY - Yen japonés', 'NZD - New Zealand Dollar' => 'NZD - Dóloar neocelandés', 'RSD - Serbian dinar' => 'RSD - Dinar serbio', - 'USD - US Dollar' => 'USD - Dólar Estadounidense', + 'USD - US Dollar' => 'USD - Dólar estadounidense', '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 concesionario', + 'Move the task to another column when assigned to a user' => 'Mover la tarea a otra columna al asignarse a un usuario', + 'Move the task to another column when assignee is cleared' => 'Mover la tarea a otra columna al quitar el responsable', 'Source column' => 'Columna fuente', '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.', + 'Task transitions export' => 'Exportar 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 transició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', '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.', - 'Webhook URL' => 'URL de Disparador Web (webhook)', - '%s remove the assignee of the task %s' => '%s quita el concesionario de la tarea %s', + 'The currency rate have been added successfully.' => 'El cambio de moneda se ha añadido correctamente.', + 'Unable to add this currency rate.' => 'No se puede añadir este cambio de moneda.', + 'Webhook URL' => 'URL del disparador web (webhook)', + '%s remove the assignee of the task %s' => '%s quita el responsable de la tarea %s', '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', + 'Check two factor authentication code' => 'Revisar código de autenticación en dos pasos', + 'The two factor authentication code is not valid.' => 'El código de autenticación en dos pasos no es válido.', + 'The two factor authentication code is valid.' => 'El código de autenticación en dos pasos es válido.', 'Code' => 'Código', - 'Two factor authentication' => 'Autenticación de dos factores', + 'Two factor authentication' => 'Autenticación en dos pasos', 'This QR code contains the key URI: ' => 'Este código QR contiene la clave URI: ', 'Check my code' => 'Revisar mi código', 'Secret key: ' => 'Clave secreta: ', 'Test your device' => 'Probar su dispositivo', 'Assign a color when the task is moved to a specific column' => 'Asignar un color al mover la tarea a una columna específica', '%s via Kanboard' => '%s vía Kanboard', - 'Burndown chart for "%s"' => 'Trabajo pendiente para "%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)', + 'This chart show the task complexity over the time (Work Remaining).' => 'Este diagrama muestra la complejidad de la tarea 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', + 'Screenshot uploaded successfully.' => 'Pantallazo cargado correctamente.', 'SEK - Swedish Krona' => 'SEK - Corona sueca', 'Identifier' => 'Identificador', - 'Disable two factor authentication' => 'Desactivar la autenticación de dos factores', - 'Do you really want to disable the two factor authentication for this user: "%s"?' => '¿Realmentes quiere desactuvar la autenticación de dos factores para este usuario: "%s?"', - 'Edit link' => 'Editar enlace', + 'Disable two factor authentication' => 'Desactivar la autenticación en dos pasos', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => '¿Realmente desea desactivar la autenticación en dos pasos para este usuario: «%s»?', + 'Edit link' => 'Modificar enlace', 'Start to type task title...' => 'Empiece a escribir el título de la tarea...', - 'A task cannot be linked to itself' => 'Una tarea no puede se enlazada con sigo misma', + 'A task cannot be linked to itself' => 'Una tarea no se puede enlazar con sigo misma', 'The exact same link already exists' => 'El mismo enlace ya existe', 'Recurrent task is scheduled to be generated' => 'Tarea recurrente programada para ser generada', 'Score' => 'Puntuación', 'The identifier must be unique' => 'El identificador debe ser único', - 'This linked task id doesn\'t exists' => 'El id de tarea no existe', + 'This linked task id doesn\'t exists' => 'El identificador de tarea no existe', 'This value must be alphanumeric' => 'Este valor debe ser alfanumérico', - 'Edit recurrence' => 'Editar repetición', - 'Generate recurrent task' => 'Generar tarea recurrente', - 'Trigger to generate recurrent task' => 'Disparador para generar tarea recurrente', + 'Edit recurrence' => 'Modificar repetición', + 'Generate recurrent task' => 'Generar una tarea recurrente', + 'Trigger to generate recurrent task' => 'Disparador para generar una tarea recurrente', 'Factor to calculate new due date' => 'Factor para calcular la nueva fecha de entrega', 'Timeframe to calculate new due date' => 'Calendario para calcular la nueva fecha de entrega', 'Base date to calculate new due date' => 'Fecha base para calcular la nueva fecha de entrega', 'Action date' => 'Fecha de la acción', - 'Base date to calculate new due date: ' => 'Fecha base para calcular la nueva fecha de entrega:', - 'This task has created this child task: ' => 'Esta tarea ha cerado esta tarea hija:', + 'Base date to calculate new due date: ' => 'Fecha base para calcular la nueva fecha de entrega: ', + 'This task has created this child task: ' => 'Esta tarea ha creado esta tarea hija: ', 'Day(s)' => 'Día(s)', 'Existing due date' => 'Fecha de entrega existente', - 'Factor to calculate new due date: ' => 'Factor para calcular la nueva fecha de entrega:', + 'Factor to calculate new due date: ' => 'Factor para calcular la nueva fecha de entrega: ', 'Month(s)' => 'Mes(es)', 'Recurrence' => 'Repetición', 'This task has been created by: ' => 'Esta tarea ha sido creada por: ', 'Recurrent task has been generated:' => 'Tarea recurrente generada:', - 'Timeframe to calculate new due date: ' => 'Calendario para calcular la nueva fecha de entrega:', - 'Trigger to generate recurrent task: ' => 'Disparador para generar tarea recurrente', + 'Timeframe to calculate new due date: ' => 'Calendario para calcular la nueva fecha de entrega: ', + 'Trigger to generate recurrent task: ' => 'Disparador para generar una tarea recurrente: ', 'When task is closed' => 'Cuando la tarea es cerrada', 'When task is moved from first column' => 'Cuando la tarea es movida desde la primera columna', 'When task is moved to last column' => 'Cuando la tarea es movida a la última columna', 'Year(s)' => 'Año(s)', - 'Calendar settings' => 'Parámetros del Calendario', - 'Project calendar view' => 'Vista de Calendario para el Proyecto', - 'Project settings' => 'Parámetros del Proyecto', - 'Show subtasks based on the time tracking' => 'Mostrar subtareas en base al seguimiento de tiempo', + 'Calendar settings' => 'Preferencias del calendario', + 'Project calendar view' => 'Vista de calendario del proyecto', + 'Project settings' => 'Preferencias del proyecto', + 'Show subtasks based on the time tracking' => 'Mostrar subtareas en base al seguimiento temporal', 'Show tasks based on the creation date' => 'Mostrar tareas en base a la fecha de creación', - 'Show tasks based on the start date' => 'Mostrar tareas en base a la fecha de comienzo', + 'Show tasks based on the start date' => 'Mostrar tareas en base a la fecha de inicio', 'Subtasks time tracking' => 'Seguimiento de tiempo en subtareas', - 'User calendar view' => 'Vista de Calendario para el Usuario', - 'Automatically update the start date' => 'Actualizar automáticamente la fecha de comienzo', + 'User calendar view' => 'Vista de calendario del usuario', + 'Automatically update the start date' => 'Actualizar automáticamente la fecha de inicio', 'iCal feed' => 'Fuente iCal', 'Preferences' => 'Preferencias', 'Security' => 'Seguridad', - 'Two factor authentication disabled' => 'Autenticación de dos factores deshabilitada', - 'Two factor authentication enabled' => 'Autenticación de dos factores habilitada', - 'Unable to update this user.' => 'Imposible actualizar este usuario.', + 'Two factor authentication disabled' => 'Autenticación en dos pasos deshabilitada', + 'Two factor authentication enabled' => 'Autenticación en dos pasos habilitada', + 'Unable to update this user.' => 'No se puede actualizar este usuario.', 'There is no user management for private projects.' => 'No hay gestión de usuarios para proyectos privados.', 'User that will receive the email' => 'Usuario que recibirá el correo', 'Email subject' => 'Asunto del correo', 'Date' => 'Fecha', 'Add a comment log when moving the task between columns' => 'Añadir un comentario al mover la tarea entre columnas', - 'Move the task to another column when the category is changed' => 'Mover la tarea a otra columna cuando cambia la categoría', + 'Move the task to another column when the category is changed' => 'Mover la tarea a otra columna cuando cambie la categoría', 'Send a task by email to someone' => 'Enviar una tarea a alguien por correo', 'Reopen a task' => 'Reabrir tarea', 'Column change' => 'Cambio de columna', 'Position change' => 'Cambio de posición', 'Swimlane change' => 'Cambio de calle', - 'Assignee change' => 'Cambio de concesionario', + 'Assignee change' => 'Cambio de responsable', '[%s] Overdue tasks' => '[%s] Tareas vencidas', 'Notification' => 'Notificación', '%s moved the task #%d to the first swimlane' => '%s movió la tarea #%d a la primera calle', - '%s moved the task #%d to the swimlane "%s"' => '%s movió la tarea #%d a la calle "%s"', + '%s moved the task #%d to the swimlane "%s"' => '%s movió la tarea #%d a la calle «%s»', 'Swimlane' => 'Calle', 'Gravatar' => 'Gravatar', '%s moved the task %s to the first swimlane' => '%s movió la tarea %s a la primera calle', - '%s moved the task %s to the swimlane "%s"' => '%s movió la tarea %s a la calle "%s"', - 'This report contains all subtasks information for the given date range.' => 'Este informe contiene todas la información de las subtareas para el rango proporcionado de fechas.', - 'This report contains all tasks information for the given date range.' => 'Este informe contiene todas la información de las tareas para el rango proporcionado de fechas.', + '%s moved the task %s to the swimlane "%s"' => '%s movió la tarea %s a la calle «%s»', + 'This report contains all subtasks information for the given date range.' => 'Este informe contiene toda la información de las subtareas para el rango de fechas proporcionado.', + 'This report contains all tasks information for the given date range.' => 'Este informe contiene toda la información de las tareas para el rango de fechas proporcionado.', 'Project activities for %s' => 'Actividades del proyecto para %s', 'view the board on Kanboard' => 'ver el tablero en Kanboard', 'The task have been moved to the first swimlane' => 'Se ha movido la tarea a la primera calle', - 'The task have been moved to another swimlane:' => 'Se ha movido la tarea a otra calle', - 'Overdue tasks for the project "%s"' => 'Tareas atrasadas para el proyecto "%s"', + 'The task have been moved to another swimlane:' => 'Se ha movido la tarea a otra calle:', 'New title: %s' => 'Nuevo título: %s', 'The task is not assigned anymore' => 'La tarea ya no está asignada', - 'New assignee: %s' => 'Nuevo concesionario: %s', + 'New assignee: %s' => 'Nuevo responsable: %s', 'There is no category now' => 'En este momento no hay categorías', 'New category: %s' => 'Nueva categoría: %s', 'New color: %s' => 'Nuevo color: %s', @@ -722,110 +710,109 @@ return array( 'Recurrence settings have been modified' => 'Se han modificado los valores recurrentes', 'Time spent changed: %sh' => 'Se ha cambiado el tiempo empleado: %sh', 'Time estimated changed: %sh' => 'Se ha cambiado el tiempo estimado: %sh', - 'The field "%s" have been updated' => 'Se ha actualizado el campo "%s"', - 'The description has been modified:' => 'Se ha modificado la descripción', - 'Do you really want to close the task "%s" as well as all subtasks?' => '¿De verdad que quiere cerra la tarea "%s" así como todas las subtareas?', + 'The field "%s" have been updated' => 'Se ha actualizado el campo «%s»', + 'The description has been modified:' => 'Se ha modificado la descripción:', + 'Do you really want to close the task "%s" as well as all subtasks?' => '¿Realmente desea cerrar la tarea «%s» así como todas las subtareas?', 'I want to receive notifications for:' => 'Deseo recibir notificaciones para:', 'All tasks' => 'Todas las tareas', 'Only for tasks assigned to me' => 'Sólo para las tareas que me han sido asignadas', - 'Only for tasks created by me' => 'Sólo para las taread creadas por mí', - 'Only for tasks created by me and assigned to me' => 'Sólo para las tareas credas por mí y que me han sido asignadas', + 'Only for tasks created by me' => 'Sólo para las tareas creadas por mí', + 'Only for tasks created by me and assigned to me' => 'Sólo para las tareas creadas por mí y que me han sido asignadas', '%%Y-%%m-%%d' => '%%d/%%M/%%Y', 'Total for all columns' => 'Total para todas las columnas', 'You need at least 2 days of data to show the chart.' => 'Necesitas al menos 2 días de datos para mostrar el gráfico.', '<15m' => '<15m', '<30m' => '<30m', 'Stop timer' => 'Parar temporizador', - 'Start timer' => 'Arrancar temporizador', + 'Start timer' => 'Iniciar temporizador', 'Add project member' => 'Añadir miembro al proyecto', - 'Enable notifications' => 'Activar notificaciones', 'My activity stream' => 'Mi flujo de actividad', 'My calendar' => 'Mi calendario', 'Search tasks' => 'Buscar tareas', 'Reset filters' => 'Limpiar filtros', 'My tasks due tomorrow' => 'Mis tareas a entregar mañana', - 'Tasks due today' => 'Tareas a antregar hoy', - 'Tasks due tomorrow' => 'Taraes a entregar mañana', + 'Tasks due today' => 'Tareas a entregar hoy', + 'Tasks due tomorrow' => 'Tareas a entregar mañana', 'Tasks due yesterday' => 'Tareas a entregar ayer', 'Closed tasks' => 'Tareas cerradas', 'Open tasks' => 'Tareas abiertas', 'Not assigned' => 'No asignada', - 'View advanced search syntax' => 'Ver sintáxis avanzada de búsqueda', + 'View advanced search syntax' => 'Ver sintaxis de búsqueda avanzada', 'Overview' => 'Resumen', - 'Board/Calendar/List view' => 'Vista de Tablero/Calendario/Lista', + 'Board/Calendar/List view' => 'Vista de tablero/calendario/lista', 'Switch to the board view' => 'Cambiar a vista de tablero', 'Switch to the calendar view' => 'Cambiar a vista de calendario', 'Switch to the list view' => 'Cambiar a vista de lista', 'Go to the search/filter box' => 'Ir a caja de buscar/filtrar', 'There is no activity yet.' => 'Aún no hay actividades.', - 'No tasks found.' => 'No se ha hallado tarea alguna.', + 'No tasks found.' => 'No se ha encontrado ninguna tarea.', 'Keyboard shortcut: "%s"' => 'Atajo de teclado: %s', 'List' => 'Lista', 'Filter' => 'Filtro', 'Advanced search' => 'Búsqueda avanzada', - 'Example of query: ' => 'Ejemplo de query: ', + 'Example of query: ' => 'Ejemplo de consulta: ', 'Search by project: ' => 'Buscar por proyecto: ', 'Search by column: ' => 'Buscar por columna: ', - 'Search by assignee: ' => 'Buscar por concesionario: ', + 'Search by assignee: ' => 'Buscar por responsable: ', 'Search by color: ' => 'Buscar por color: ', 'Search by category: ' => 'Buscar por categoría: ', 'Search by description: ' => 'Buscar por descripción: ', 'Search by due date: ' => 'Buscar por fecha de entrega: ', - 'Lead and Cycle time for "%s"' => 'Plazo de Entrega y Ciclo para "%s"', - 'Average time spent into each column for "%s"' => 'Tiempo medio empleado en cada columna para "%s"', + 'Lead and Cycle time for "%s"' => 'Plazo de entrega y ciclo para «%s»', + 'Average time spent into each column for "%s"' => 'Tiempo medio empleado en cada columna para «%s»', 'Average time spent into each column' => 'Tiempo medio empleado en cada columna', 'Average time spent' => 'Tiempo medio empleado', 'This chart show the average time spent into each column for the last %d tasks.' => 'Esta gráfica muestra el tiempo medio empleado en cada columna para las últimas %d tareas.', - 'Average Lead and Cycle time' => 'Plazo Medio de Entrega y de Ciclo', - 'Average lead time: ' => 'Plazo Medio de entrega: ', - 'Average cycle time: ' => 'Tiempo Medio de Ciclo: ', - 'Cycle Time' => 'Tiempo de Ciclo', - 'Lead Time' => 'Plazo de Entrega', - 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Esta gráfica muestra el plazo medio de entrega y de ciclo para las %d últimas tareas transcurridas.', + 'Average Lead and Cycle time' => 'Plazo medio de entrega y de ciclo', + 'Average lead time: ' => 'Plazo medio de entrega: ', + 'Average cycle time: ' => 'Tiempo medio de ciclo: ', + 'Cycle Time' => 'Tiempo de ciclo', + 'Lead Time' => 'Plazo de entrega', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Esta gráfica muestra el plazo medio de entrega y de ciclo para las %d últimas tareas.', 'Average time into each column' => 'Tiempo medio en cada columna', 'Lead and cycle time' => 'Plazo de entrega y de ciclo', 'Lead time: ' => 'Plazo de entrega: ', - 'Cycle time: ' => 'Tiempo de Ciclo: ', + 'Cycle time: ' => 'Tiempo de ciclo: ', 'Time spent into each column' => 'Tiempo empleado en cada columna', - 'The lead time is the duration between the task creation and the completion.' => 'El plazo de entrega es la duración entre la creación de la tarea su terminación.', - 'The cycle time is the duration between the start date and the completion.' => 'El tiempo de ciclo es la duración entre la fecha de inicio y su terminación.', - 'If the task is not closed the current time is used instead of the completion date.' => 'Si la tarea no se cierra, se usa la fecha actual en lugar de la de terminación.', + 'The lead time is the duration between the task creation and the completion.' => 'El plazo de entrega es la duración entre la creación de la tarea su finalización.', + 'The cycle time is the duration between the start date and the completion.' => 'El tiempo de ciclo es la duración entre la fecha de inicio y su finalización.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Si la tarea no se cierra, se usa la fecha actual en lugar de la de finalización.', 'Set automatically the start date' => 'Poner la fecha de inicio de forma automática', - 'Edit Authentication' => 'Editar autenticación', + 'Edit Authentication' => 'Modificar autenticación', 'Remote user' => 'Usuario remoto', - 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Los usuarios remotos no almacenan sus contraseñas en la base de datos Kanboard, por ejemplo: cuentas de LDAP, Google y Github', - 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si marcas la caja de edición "Desactivar formulario de ingreso", se ignoran las credenciales entradas en el formulario de ingreso.', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Los usuarios remotos no almacenan sus contraseñas en la base de datos Kanboard, por ejemplo: cuentas de LDAP, Google y Github.', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Si marcas la opción "Desactivar formulario de ingreso", se ignoran las credenciales introducidas en el formulario de ingreso.', 'New remote user' => 'Nuevo usuario remoto', 'New local user' => 'Nuevo usuario local', - 'Default task color' => 'Color por defecto de tarea', - 'This feature does not work with all browsers.' => 'Esta característica no funciona con todos los navegadores', - 'There is no destination project available.' => 'No está disponible proyecto destino', - 'Trigger automatically subtask time tracking' => 'Disparar de forma automática seguimiento temporal de subtarea', + 'Default task color' => 'Color de la tarea por defecto', + 'This feature does not work with all browsers.' => 'Esta característica no funciona en todos los navegadores.', + 'There is no destination project available.' => 'No está disponible proyecto de destino.', + 'Trigger automatically subtask time tracking' => 'Disparar de forma automática el seguimiento temporal de subtarea', 'Include closed tasks in the cumulative flow diagram' => 'Incluir tareas cerradas en el diagrama de flujo acumulado', 'Current swimlane: %s' => 'Calle en curso: %s', 'Current column: %s' => 'Columna en curso: %s', 'Current category: %s' => 'Categoría en curso: %s', 'no category' => 'no hay categoría', - 'Current assignee: %s' => 'Concesionario en curso: %s', + 'Current assignee: %s' => 'Responsable en curso: %s', 'not assigned' => 'sin asignar', 'Author:' => 'Autor:', 'contributors' => 'contribuyentes', 'License:' => 'Licencia:', 'License' => 'Licencia', - 'Enter the text below' => 'Digita el texto de abajo', + 'Enter the text below' => 'Introduzca el texto a continuación', 'Gantt chart for %s' => 'Diagrama de Gantt para %s', - 'Sort by position' => 'Clasificado mediante posición', - 'Sort by date' => 'Clasificado mediante fecha', + 'Sort by position' => 'Ordenar por posición', + 'Sort by date' => 'Ordenar por fecha', 'Add task' => 'Añadir tarea', 'Start date:' => 'Fecha de inicio:', 'Due date:' => 'Fecha de entrega:', 'There is no start date or due date for this task.' => 'No hay fecha de inicio o de entrega para esta tarea.', - 'Moving or resizing a task will change the start and due date of the task.' => 'El mover o redimensionar una tarea cambiará la fecha inicio y de entrega de la misma.', + 'Moving or resizing a task will change the start and due date of the task.' => 'Mover o redimensionar una tarea cambiará la fecha inicio y de entrega de la misma.', 'There is no task in your project.' => 'No hay tareas en su proyecto.', - 'Gantt chart' => 'Digrama de Gantt', - 'People who are project managers' => 'Usuarios que son administradores de proyecto', - 'People who are project members' => 'Usuarios que son miembros de proyecto', - 'NOK - Norwegian Krone' => 'NOK - Coronoa Noruega', + 'Gantt chart' => 'Diagrama de Gantt', + 'People who are project managers' => 'Usuarios que son administradores del proyecto', + 'People who are project members' => 'Usuarios que son miembros del proyecto', + 'NOK - Norwegian Krone' => 'NOK - Corona Noruega', 'Show this column' => 'Mostrar esta columna', 'Hide this column' => 'Ocultar esta columna', 'open file' => 'abrir fichero', @@ -833,80 +820,80 @@ return array( 'Users overview' => 'Resumen de usuarios', 'Members' => 'Miembros', 'Shared project' => 'Proyecto compartido', - 'Project managers' => 'Administradores de proyecto', + 'Project managers' => 'Administradores del proyecto', 'Gantt chart for all projects' => 'Diagrama de Gantt para todos los proyectos', 'Projects list' => 'Lista de proyectos', 'Gantt chart for this project' => 'Diagrama de Gantt para este proyecto', 'Project board' => 'Tablero del proyecto', - 'End date:' => 'Fecha final', + 'End date:' => 'Fecha de fin:', 'There is no start date or end date for this project.' => 'No existe fecha de inicio o de fin para este proyecto.', - 'Projects Gantt chart' => 'Diagramas de Gantt de los proyectos', - 'Change task color when using a specific task link' => 'Cambiar colo de la tarea al usar un enlace específico a tarea', - 'Task link creation or modification' => 'Creación o modificación de enlace a tarea', + 'Projects Gantt chart' => 'Diagrama de Gantt de los proyectos', + 'Change task color when using a specific task link' => 'Cambiar el color de la tarea al usar un enlace específico a tarea', + 'Task link creation or modification' => 'Creación o modificación del enlace a tarea', 'Milestone' => 'Hito', 'Documentation: %s' => 'Documentación: %s', - 'Switch to the Gantt chart view' => 'Conmutar a vista de diagrama de Gantt', - 'Reset the search/filter box' => 'Limpiar la caja del filtro de búsqueda', + 'Switch to the Gantt chart view' => 'Cambiar a vista de diagrama de Gantt', + 'Reset the search/filter box' => 'Limpiar el filtro de búsqueda', 'Documentation' => 'Documentación', 'Table of contents' => 'Tabla de contenido', 'Gantt' => 'Gantt', 'Author' => 'Autor', 'Version' => 'Versión', 'Plugins' => 'Plugins', - 'There is no plugin loaded.' => 'No hay ningún plugin cargado', + 'There is no plugin loaded.' => 'No hay ningún plugin cargado.', 'Set maximum column height' => 'Establecer altura máxima de la columna', 'Remove maximum column height' => 'Eliminar altura máxima de la columna', 'My notifications' => 'Mis notificaciones', 'Custom filters' => 'Filtros personalizados', - 'Your custom filter have been created successfully.' => 'Tus filtros personalizados han sido creados exitosamente', - 'Unable to create your custom filter.' => 'No se ha podido crear tu filtro personalizado', - 'Custom filter removed successfully.' => 'Filtro personalizado ha sido eliminado exitosamente', - 'Unable to remove this custom filter.' => 'No se ha podido eliminar tu filtro personalizado', + 'Your custom filter have been created successfully.' => 'Tus filtros personalizados han sido creados correctamente.', + 'Unable to create your custom filter.' => 'No se ha podido crear tu filtro personalizado.', + 'Custom filter removed successfully.' => 'El filtro personalizado ha sido eliminado correctamente.', + 'Unable to remove this custom filter.' => 'No se ha podido eliminar tu filtro personalizado.', 'Edit custom filter' => 'Modificar filtro personalizado', - 'Your custom filter have been updated successfully.' => 'Tu filtro personalizado ha sido actualizado exitosamente', - 'Unable to update custom filter.' => 'No se ha podido actualizar tu filtro personalizado', + 'Your custom filter have been updated successfully.' => 'Tu filtro personalizado ha sido actualizado correctamente.', + 'Unable to update custom filter.' => 'No se ha podido actualizar tu filtro personalizado.', 'Web' => 'Web', 'New attachment on task #%d: %s' => 'Nuevo adjunto en la tarea #%d: %s', 'New comment on task #%d' => 'Nuevo comentario en la tarea #%d', 'Comment updated on task #%d' => 'Comentario actualizado en la tarea #%d', 'New subtask on task #%d' => 'Nueva subtarea en la tarea #%d', - 'Subtask updated on task #%d' => 'La subtarea en la tarea #%d ha sido actualizada', + 'Subtask updated on task #%d' => 'Subtarea actualizada en la tarea #%d', 'New task #%d: %s' => 'Nueva tarea #%d: %s', 'Task updated #%d' => 'Tarea actualizada #%d', - 'Task #%d closed' => 'Tarea #%d ha sido cerrada', - 'Task #%d opened' => 'Tarea #%d ha sido abierta', - 'Column changed for task #%d' => 'Columna para tarea #%d ha sido cambiada', + 'Task #%d closed' => 'Tarea #%d cerrada', + 'Task #%d opened' => 'Tarea #%d abierta', + 'Column changed for task #%d' => 'Columna cambiada para la tarea #%d', 'New position for task #%d' => 'Nueva posición para tarea #%d', - 'Swimlane changed for task #%d' => 'Se cambió el swimlane de la tarea #%d', - 'Assignee changed on task #%d' => 'Se cambió el asignado de la tarea #%d', + 'Swimlane changed for task #%d' => 'Se cambió la calle de la tarea #%d', + 'Assignee changed on task #%d' => 'Se cambió el responsable de la tarea #%d', '%d overdue tasks' => '%d tareas atrasadas', 'Task #%d is overdue' => 'La tarea #%d está atrasada', - 'No new notifications.' => 'No hay nuevas notificaciones', + 'No new notifications.' => 'No hay nuevas notificaciones.', 'Mark all as read' => 'Marcar todo como leído', 'Mark as read' => 'Marcar como leído', - 'Total number of tasks in this column across all swimlanes' => 'Número total de tareas en esta columna por todas las swimlanes', - 'Collapse swimlane' => 'Contraer swimlane', - 'Expand swimlane' => 'Ampliar swimlane', + 'Total number of tasks in this column across all swimlanes' => 'Número total de tareas en esta columna a través de todas las calles', + 'Collapse swimlane' => 'Contraer calle', + 'Expand swimlane' => 'Ampliar calle', 'Add a new filter' => 'Añadir nuevo filtro', 'Share with all project members' => 'Compartir con todos los miembros del proyecto', 'Shared' => 'Compartido', - 'Owner' => 'Dueño', + 'Owner' => 'Propietario', 'Unread notifications' => 'Notificaciones sin leer', - 'Notification methods:' => 'Métodos de notificación', + 'Notification methods:' => 'Métodos de notificación:', 'Import tasks from CSV file' => 'Importar tareas desde archivo CSV', 'Unable to read your file' => 'No es posible leer el archivo', - '%d task(s) have been imported successfully.' => '%d tarea(s) han sido importadas exitosamente', - 'Nothing have been imported!' => 'No se ha importado nada!', + '%d task(s) have been imported successfully.' => '%d tarea(s) han sido importadas correctamente.', + 'Nothing have been imported!' => '¡No se ha importado nada!', 'Import users from CSV file' => 'Importar usuarios desde archivo CSV', - '%d user(s) have been imported successfully.' => '%d usuario(s) se han importado exitosamente', + '%d user(s) have been imported successfully.' => '%d usuario(s) se han importado correctamente.', 'Comma' => 'Coma', 'Semi-colon' => 'Punto y coma', 'Tab' => 'Tabulación', - 'Vertical bar' => 'Pleca', + 'Vertical bar' => 'Barra vertical', 'Double Quote' => 'Comilla doble', - 'Single Quote' => 'Comilla sencilla', - '%s attached a file to the task #%d' => '%s adjuntó un archivo a la tarea #%d', - 'There is no column or swimlane activated in your project!' => 'No hay ninguna columna o swimlane activada en su proyecto!', + 'Single Quote' => 'Comilla simple', + '%s attached a file to the task #%d' => '%s adjuntó un archivo en la tarea #%d', + 'There is no column or swimlane activated in your project!' => '¡No hay ninguna columna o calle activada en su proyecto!', 'Append filter (instead of replacement)' => 'Añadir filtro (en vez de reemplazar)', 'Append/Replace' => 'Añadir/Reemplazar', 'Append' => 'Añadir', @@ -919,92 +906,92 @@ return array( 'CSV File' => 'Archivo CSV', 'Instructions' => 'Indicaciones', 'Your file must use the predefined CSV format' => 'Su archivo debe utilizar el formato CSV predeterminado', - 'Your file must be encoded in UTF-8' => 'Su archivo debe ser codificado en UTF-8', + 'Your file must be encoded in UTF-8' => 'Su archivo debe estar codificado en UTF-8', 'The first row must be the header' => 'La primera fila debe ser el encabezado', 'Duplicates are not verified for you' => 'Los duplicados no serán verificados', 'The due date must use the ISO format: YYYY-MM-DD' => 'La fecha de entrega debe utilizar el formato ISO: AAAA-MM-DD', 'Download CSV template' => 'Descargar plantilla CSV', - 'No external integration registered.' => 'No se ha registrado integración externa', + 'No external integration registered.' => 'No se ha registrado integración externa.', 'Duplicates are not imported' => 'Los duplicados no son importados', - 'Usernames must be lowercase and unique' => 'Los nombres de usuario deben ser únicos y contener sólo minúsculas', + 'Usernames must be lowercase and unique' => 'Los nombres de usuario deben ser únicos y en minúsculas', 'Passwords will be encrypted if present' => 'Las contraseñas serán cifradas si es que existen', - '%s attached a new file to the task %s' => '%s adjuntó un nuevo archivo a la tarea %s', + '%s attached a new file to the task %s' => '%s adjuntó un nuevo archivo en la tarea %s', 'Link type' => 'Tipo de enlace', 'Assign automatically a category based on a link' => 'Asignar una categoría automáticamente basado en un enlace', - 'BAM - Konvertible Mark' => 'BAM - marco convertible', - 'Assignee Username' => 'Nombre de usuario del concesionario', - 'Assignee Name' => 'Nombre del concesionario', + 'BAM - Konvertible Mark' => 'BAM - Marco convertible', + 'Assignee Username' => 'Nombre de usuario del responsable', + 'Assignee Name' => 'Nombre del responsable', 'Groups' => 'Grupos', 'Members of %s' => 'Miembros de %s', 'New group' => 'Nuevo grupo', - 'Group created successfully.' => 'Grupo creado exitosamente', - 'Unable to create your group.' => 'No es posible crear el grupo', - 'Edit group' => 'Editar grupo', - 'Group updated successfully.' => 'Grupo actualizado exitosamente', - 'Unable to update your group.' => 'No es posible actualizar el grupo', - 'Add group member to "%s"' => 'Agregar miembro del grupo a "%s"', - 'Group member added successfully.' => 'Miembro del grupo agregado exitosamente', - 'Unable to add group member.' => 'No es posible agregar miembro del grupo', - 'Remove user from group "%s"' => 'Eliminar usuario del grupo "%s"', - 'User removed successfully from this group.' => 'Usuario eliminado exitosamente del grupo', - 'Unable to remove this user from the group.' => 'No es posible eliminar este usuario del grupo', + 'Group created successfully.' => 'Grupo creado correctamente.', + 'Unable to create your group.' => 'No es posible crear el grupo.', + 'Edit group' => 'Modificar grupo', + 'Group updated successfully.' => 'Grupo actualizado correctamente.', + 'Unable to update your group.' => 'No es posible actualizar el grupo.', + 'Add group member to "%s"' => 'Añadir un miembro del grupo a «%s»', + 'Group member added successfully.' => 'Miembro del grupo añadido correctamente.', + 'Unable to add group member.' => 'No es posible añadir el miembro del grupo.', + 'Remove user from group "%s"' => 'Eliminar usuario del grupo «%s»', + 'User removed successfully from this group.' => 'Usuario eliminado correctamente del grupo.', + 'Unable to remove this user from the group.' => 'No es posible eliminar este usuario del grupo.', 'Remove group' => 'Eliminar grupo', - 'Group removed successfully.' => 'Grupo eliminado exitosamente', - 'Unable to remove this group.' => 'No es posible eliminar este grupo', + 'Group removed successfully.' => 'Grupo eliminado correctamente.', + 'Unable to remove this group.' => 'No es posible eliminar este grupo.', 'Project Permissions' => 'Permisos del proyecto', 'Manager' => 'Administrador', - 'Project Manager' => 'Administrador de proyecto', + 'Project Manager' => 'Administrador del proyecto', 'Project Member' => 'Miembro del proyecto', - 'Project Viewer' => 'Visor de proyectos', - 'Your account is locked for %d minutes' => 'Tu cuenta ha sido bloqueada por %d minuto(s)', + 'Project Viewer' => 'Observador del proyecto', + 'Your account is locked for %d minutes' => 'Tu cuenta ha sido bloqueada durante %d minutos', 'Invalid captcha' => 'CAPTCHA inválido', 'The name must be unique' => 'El nombre debe ser único', 'View all groups' => 'Ver todos los grupos', 'View group members' => 'Ver miembros del grupo', - 'There is no user available.' => 'No hay usuario disponible', - 'Do you really want to remove the user "%s" from the group "%s"?' => '¿Realmente desea eliminar el usuario "%s" del grupo "%s"?', - 'There is no group.' => 'No hay grupo', - 'External Id' => 'ID externo', - 'Add group member' => 'Agregar miembro de grupo', - 'Do you really want to remove this group: "%s"?' => '¿Realmente desea eliminar este grupo: "%s"?', - 'There is no user in this group.' => 'No hay usuario en este grupo', + 'There is no user available.' => 'No hay usuario disponible.', + 'Do you really want to remove the user "%s" from the group "%s"?' => '¿Realmente desea eliminar el usuario «%s» del grupo «%s»?', + 'There is no group.' => 'No hay grupo.', + 'External Id' => 'Identificador externo', + 'Add group member' => 'Añadir un miembro al grupo', + 'Do you really want to remove this group: "%s"?' => '¿Realmente desea eliminar este grupo: «%s»?', + 'There is no user in this group.' => 'No hay usuario en este grupo.', 'Remove this user' => 'Eliminar este usuario', 'Permissions' => 'Permisos', 'Allowed Users' => 'Usuarios permitidos', - 'No user have been allowed specifically.' => 'Ningun usuario ha sido explícitamente permitido', + 'No user have been allowed specifically.' => 'Ningún usuario ha sido explícitamente permitido.', 'Role' => 'Rol', 'Enter user name...' => 'Ingresa nombre de usuario...', 'Allowed Groups' => 'Grupos permitidos', - 'No group have been allowed specifically.' => 'Ningun grupo ha sido explícitamente permitido', + 'No group have been allowed specifically.' => 'Ningún grupo ha sido explícitamente permitido.', 'Group' => 'Grupo', 'Group Name' => 'Nombre del grupo', 'Enter group name...' => 'Ingresa el nombre del grupo...', 'Role:' => 'Rol:', 'Project members' => 'Miembros del proyecto', - 'Compare hours for "%s"' => 'Compara horas con "%s"', + 'Compare hours for "%s"' => 'Compara horas con «%s»', '%s mentioned you in the task #%d' => '%s te mencionó en la tarea #%d', '%s mentioned you in a comment on the task #%d' => '%s te mencionó en un comentario en la tarea #%d', - 'You were mentioned in the task #%d' => 'Te mencionaron en la tarea #%d', - 'You were mentioned in a comment on the task #%d' => 'Te mencionaron en un comentario en la tarea #%d', + 'You were mentioned in the task #%d' => 'Fuiste mencionado en la tarea #%d', + 'You were mentioned in a comment on the task #%d' => 'Fuiste mencionado en un comentario de la tarea #%d', 'Mentioned' => 'Mencionado', - 'Compare Estimated Time vs Actual Time' => 'Comparar Tiempo Estimado vs Tiempo Actual', + 'Compare Estimated Time vs Actual Time' => 'Comparar tiempo estimado vs tiempo actual', 'Estimated hours: ' => 'Horas estimadas: ', 'Actual hours: ' => 'Horas actuales: ', 'Hours Spent' => 'Horas gastadas', - 'Hours Estimated' => 'Hora Estimada', - 'Estimated Time' => 'Tiempo Estimado', - 'Actual Time' => 'Tiempo Actual', + 'Hours Estimated' => 'Horas estimadas', + 'Estimated Time' => 'Tiempo estimado', + 'Actual Time' => 'Tiempo actual', 'Estimated vs actual time' => 'Tiempo estimado vs real', - 'RUB - Russian Ruble' => 'RUB - rublo ruso', - 'Assign the task to the person who does the action when the column is changed' => 'Asignar la tarea a la persona que haga la acción al cambiar de columna', + 'RUB - Russian Ruble' => 'RUB - Rublo ruso', + 'Assign the task to the person who does the action when the column is changed' => 'Asignar la tarea a la persona que hace la acción al cambiar de columna', 'Close a task in a specific column' => 'Cerrar tarea en una columna especifica', 'Time-based One-time Password Algorithm' => 'Algoritmo basado en tiempo de un solo uso', - 'Two-Factor Provider: ' => 'Proveedor de autenticación de dos factores', - 'Disable two-factor authentication' => 'Deshabilitar autenticación de dos factores', - 'Enable two-factor authentication' => 'Habilitar autenticación de dos factorse', - 'There is no integration registered at the moment.' => 'No hay ninguna integración registrada por el momento', + 'Two-Factor Provider: ' => 'Proveedor de autenticación en dos pasos: ', + 'Disable two-factor authentication' => 'Deshabilitar autenticación en dos pasos', + 'Enable two-factor authentication' => 'Habilitar autenticación en dos pasos', + 'There is no integration registered at the moment.' => 'No hay ninguna integración registrada por el momento.', 'Password Reset for Kanboard' => 'Restablecimiento de contraseña para Kanboard', - 'Forgot password?' => '¿Olvidó contraseña?', + 'Forgot password?' => '¿Olvidó la contraseña?', 'Enable "Forget Password"' => 'Habilitar "olvidar contraseña"', 'Password Reset' => 'Restablecer contraseña', 'New password' => 'Nueva contraseña', @@ -1015,19 +1002,19 @@ return array( 'Creation' => 'Creación', 'Expiration' => 'Vencimiento', 'Password reset history' => 'Historial de restablecimiento de contraseña', - 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas las tareas de la columna "%s" y el swimlane "%s" se han cerrado exitosamente', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Todas las tareas de la columna "%s" y la calle «%s» se han cerrado correctamente.', 'Do you really want to close all tasks of this column?' => '¿Realmente desea cerrar todas las tareas de esta columna?', - '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarea(s) en la columna "%s" y el swimlane "%s" será(n) cerrada(s)', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d tarea(s) en la columna "%s" y en la calle «%s» será(n) cerrada(s).', 'Close all tasks of this column' => 'Cerrar todas las tareas de esta columna', - 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Ningún plugin ha registrado un método de notificación para el proyecto. Aún puedes configurar notificaciones individuales en tu perfil de usuario', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Ningún plugin ha registrado un método de notificación para el proyecto. Aún puedes configurar notificaciones individuales en tu perfil de usuario.', 'My dashboard' => 'Mi tablero', 'My profile' => 'Mi perfil', - 'Project owner: ' => 'Dueño del proyecto', - 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'El identificador de proyecto es opcional y debe ser alfanumérico. Ejemplo: MIPROYECTO', - 'Project owner' => 'Dueño del proyecto', - 'Those dates are useful for the project Gantt chart.' => 'Esas fechas son útiles para el diagrama de Gantt', - 'Private projects do not have users and groups management.' => 'Proyectos privados no cuentan con gestión de usuarios y grupos', - 'There is no project member.' => 'No existe miembro del proyecto', + 'Project owner: ' => 'Propietario del proyecto: ', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'El identificador del proyecto es opcional y debe ser alfanumérico. Ejemplo: MIPROYECTO', + 'Project owner' => 'Propietario del proyecto', + 'Those dates are useful for the project Gantt chart.' => 'Esas fechas son útiles para el diagrama de Gantt.', + 'Private projects do not have users and groups management.' => 'Los proyectos privados no cuentan con gestión de usuarios y grupos.', + 'There is no project member.' => 'No existe miembro del proyecto.', 'Priority' => 'Prioridad', 'Task priority' => 'Prioridad de la tarea', 'General' => 'General', @@ -1035,16 +1022,16 @@ return array( 'Default priority' => 'Prioridad predeterminada', 'Lowest priority' => 'Prioridad más baja', 'Highest priority' => 'Prioridad más alta', - 'If you put zero to the low and high priority, this feature will be disabled.' => 'Si estableces la prioridad más baja y alta como cero esta función será deshabilitada', + 'If you put zero to the low and high priority, this feature will be disabled.' => 'Si estableces la prioridad más baja y más alta a cero esta función será deshabilitada.', 'Close a task when there is no activity' => 'Cerrar tarea cuando no haya actividad', 'Duration in days' => 'Duración en días', 'Send email when there is no activity on a task' => 'Enviar correo cuando no haya actividad en una tarea', - 'Unable to fetch link information.' => 'No es posible obtener información sobre el enlace', - 'Daily background job for tasks' => 'Tarea de fondo diaria para las tareas', + 'Unable to fetch link information.' => 'No es posible obtener información del enlace.', + 'Daily background job for tasks' => 'Trabajo en segundo plano diario para las tareas', 'Auto' => 'Automático', 'Related' => 'Relacionado', 'Attachment' => 'Adjunto', - 'Title not found' => 'Título no ha sido encontrado', + 'Title not found' => 'No se ha encontrado el título', 'Web Link' => 'Enlace web', 'External links' => 'Enlaces externos', 'Add external link' => 'Añadir enlace externo', @@ -1057,100 +1044,176 @@ return array( 'Copy and paste your link here...' => 'Copia y pega tu enlace aquí...', 'URL' => 'URL', 'Internal links' => 'Enlaces internos', - 'Assign to me' => 'Asignar a mí', + 'Assign to me' => 'Asignarme a mí', 'Me' => 'Yo', 'Do not duplicate anything' => 'No duplicar nada', 'Projects management' => 'Administración de proyectos', 'Users management' => 'Administración de usuarios', 'Groups management' => 'Administración de grupos', - 'Create from another project' => 'Crear de otro proyecto', + 'Create from another project' => 'Crear a partir de otro proyecto', 'open' => 'abierto', 'closed' => 'cerrado', - 'Priority:' => 'Prioridad', - 'Reference:' => 'Referencia', + 'Priority:' => 'Prioridad:', + 'Reference:' => 'Referencia:', 'Complexity:' => 'Complejidad:', - 'Swimlane:' => 'Swimlane:', + 'Swimlane:' => 'Calle:', 'Column:' => 'Columna:', 'Position:' => 'Posición:', 'Creator:' => 'Creador:', 'Time estimated:' => 'Tiempo estimado:', '%s hours' => '%s horas', 'Time spent:' => 'Tiempo gastado:', - 'Created:' => 'Creado', - 'Modified:' => 'Modificado', - 'Completed:' => 'Terminado', - 'Started:' => 'Iniciado', - 'Moved:' => 'Movido', + 'Created:' => 'Creado:', + 'Modified:' => 'Modificado:', + 'Completed:' => 'Finalizado:', + 'Started:' => 'Iniciado:', + 'Moved:' => 'Movido:', 'Task #%d' => 'Tarea #%d', - 'Date and time format' => 'Formato de hora y fecha', + 'Date and time format' => 'Formato de fecha y hora', 'Time format' => 'Formato de hora', - 'Start date: ' => 'Fecha de inicio', - 'End date: ' => 'Fecha de terminación', - 'New due date: ' => 'Nueva fecha de entrega', - 'Start date changed: ' => 'Fecha de inicio cambiada', + 'Start date: ' => 'Fecha de inicio: ', + 'End date: ' => 'Fecha de finalización: ', + 'New due date: ' => 'Nueva fecha de entrega: ', + 'Start date changed: ' => 'Fecha de inicio cambiada: ', 'Disable private projects' => 'Deshabilitar proyectos privados', - 'Do you really want to remove this custom filter: "%s"?' => '¿Realmente desea eliminar este filtro personalizado: "%s"?', - 'Remove a custom filter' => 'Eliminar filtro personalizado', - 'User activated successfully.' => 'Usuario activado exitosamente', - 'Unable to enable this user.' => 'No es posible habilitar este usuario', - 'User disabled successfully.' => 'Usuario deshabilitado exitosamente', - 'Unable to disable this user.' => 'No es posible deshabilitar este usuario', - 'All files have been uploaded successfully.' => 'Todos los archivos han sido subidos exitosamente', - 'View uploaded files' => 'Ver archivos subidos', - 'The maximum allowed file size is %sB.' => 'El límite de tamaño de archivo permitido para subir es %sB.', - 'Choose files again' => 'Eligir archivos de nuevo', + 'Do you really want to remove this custom filter: "%s"?' => '¿Realmente desea eliminar este filtro personalizado: «%s»?', + 'Remove a custom filter' => 'Eliminar el filtro personalizado', + 'User activated successfully.' => 'Usuario activado correctamente.', + 'Unable to enable this user.' => 'No es posible habilitar este usuario.', + 'User disabled successfully.' => 'Usuario deshabilitado correctamente.', + 'Unable to disable this user.' => 'No es posible deshabilitar este usuario.', + 'All files have been uploaded successfully.' => 'Todos los archivos han sido cargados correctamente.', + 'View uploaded files' => 'Ver archivos cargados', + 'The maximum allowed file size is %sB.' => 'El tamaño máximo de archivo es %sB.', + 'Choose files again' => 'Elegir archivos de nuevo', 'Drag and drop your files here' => 'Arrastra y suelta tus archivos aquí', - 'choose files' => 'Elegir archivos', + 'choose files' => 'elegir archivos', 'View profile' => 'Ver perfil', 'Two Factor' => 'Dos factores', 'Disable user' => 'Deshabilitar usuario', - 'Do you really want to disable this user: "%s"?' => '¿Realmente desea deshabilitar este usuario: "%s"?', + 'Do you really want to disable this user: "%s"?' => '¿Realmente desea deshabilitar este usuario: «%s»?', 'Enable user' => 'Habilitar usuario', - 'Do you really want to enable this user: "%s"?' => '¿Realmente desea habilitar este usuario: "%s"?', + 'Do you really want to enable this user: "%s"?' => '¿Realmente desea habilitar este usuario: «%s»?', 'Download' => 'Descargar', 'Uploaded: %s' => 'Subido: %s', 'Size: %s' => 'Tamaño: %s', 'Uploaded by %s' => 'Subido por %s', 'Filename' => 'Nombre de archivo', 'Size' => 'Tamaño', - 'Column created successfully.' => 'Columna creada exitosamente', + 'Column created successfully.' => 'Columna creada correctamente.', 'Another column with the same name exists in the project' => 'Ya existe una columna con el mismo nombre en el proyecto', 'Default filters' => 'Filtros predeterminados', - 'Your board doesn\'t have any column!' => '¡Tu tablero no tiene ninguna columna!', + 'Your board doesn\'t have any columns!' => '¡Tu tablero no tiene ninguna columna!', 'Change column position' => 'Cambiar posición de la columna', 'Switch to the project overview' => 'Cambiar a vista general del proyecto', 'User filters' => 'Usar filtros', - 'Category filters' => 'Categoría y filtros', - 'Upload a file' => 'Subir archivo', + 'Category filters' => 'Filtros de categoría', + 'Upload a file' => 'Cargar archivo', 'View file' => 'Ver archivo', 'Last activity' => 'Última actividad', 'Change subtask position' => 'Cambiar posición de la subtarea', - 'This value must be greater than %d' => 'Este valor debe ser mayor a %d', - 'Another swimlane with the same name exists in the project' => 'Ya existe otro swimlane con el mismo nombre en el proyecto', - 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Ejemplo: http://ejemplo.kanboard.net/ (Usado para generar URLs absolutas)', - 'Actions duplicated successfully.' => 'Acción duplicada con exito.', - 'Unable to duplicate actions.' => 'No se ha podido duplicar la acción.', + 'This value must be greater than %d' => 'Este valor debe ser mayor que %d', + 'Another swimlane with the same name exists in the project' => 'Ya existe otra calle con el mismo nombre en el proyecto', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Ejemplo: http://ejemplo.kanboard.net/ (usado para generar URLs absolutas)', + 'Actions duplicated successfully.' => 'Acciones duplicadas con éxito.', + 'Unable to duplicate actions.' => 'No se han podido duplicar las acciones.', 'Add a new action' => 'Añadir una nueva acción', 'Import from another project' => 'Importar de otro proyecto', 'There is no action at the moment.' => 'No hay ninguna acción en este momento.', 'Import actions from another project' => 'Importar acciones de otro proyecto', 'There is no available project.' => 'No hay proyectos disponibles.', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', - // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', - // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', - // 'Menu' => '', - // 'Set start date' => '', - // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'Local File' => 'Archivo local', + 'Configuration' => 'Configuración', + 'PHP version:' => 'Versión de PHP:', + 'PHP SAPI:' => 'PHP SAPI:', + 'OS version:' => 'Sistema operativo:', + 'Database version:' => 'Versión de la base de datos:', + 'Browser:' => 'Navegador web:', + 'Task view' => 'Ver tarea', + 'Edit task' => 'Modificar tarea', + 'Edit description' => 'Modificar descripción', + 'New internal link' => 'Nuevo enlace interno', + 'Display list of keyboard shortcuts' => 'Mostrar la lista de atajos de teclado', + 'Menu' => 'Menú', + 'Set start date' => 'Establecer fecha de inicio', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Cargar imagen de avatar', + 'Remove my image' => 'Eliminar imagen', + 'The OAuth2 state parameter is invalid' => 'El parámetro estado de OAuth2 es inválido', + 'User not found.' => 'Usuario no encontrado.', + 'Search in activity stream' => 'Buscar en el río de actividad', + 'My activities' => 'Mis actividades', + 'Activity until yesterday' => 'Actividad hasta ayer', + 'Activity until today' => 'Actividad hasta hoy', + 'Search by creator: ' => 'Buscar por creador: ', + 'Search by creation date: ' => 'Buscar por fecha de creación: ', + 'Search by task status: ' => 'Buscar por estado de la tarea: ', + 'Search by task title: ' => 'Buscar por título de la tarea: ', + 'Activity stream search' => 'Búsqueda en el río de actividad', + 'Projects where "%s" is manager' => 'Proyectos donde «%s» es administrador', + 'Projects where "%s" is member' => 'Proyectos donde «%s» es miembro', + 'Open tasks assigned to "%s"' => 'Tareas abiertas asignadas a «%s»', + 'Closed tasks assigned to "%s"' => 'Tareas cerradas asignadas a «%s»', + 'Assign automatically a color based on a priority' => 'Asignar automáticamente un color basado en la prioridad', + 'Overdue tasks for the project(s) "%s"' => 'Tareas atrasadas para el proyecto(s) «%s»', + 'Upload files' => 'Cargar archivos', + 'Installed Plugins' => 'Plugins instalados', + 'Plugin Directory' => 'Directorio de plugins', + 'Plugin installed successfully.' => 'Plugin instalado correctamente.', + 'Plugin updated successfully.' => 'Plugin actualizado correctamente.', + 'Plugin removed successfully.' => 'Plugin eliminado correctamente.', + 'Subtask converted to task successfully.' => 'Subtarea convertida en tarea correctamente.', + 'Unable to convert the subtask.' => 'No se puede convertir la subtarea.', + 'Unable to extract plugin archive.' => 'No se puede extraer el plugin.', + 'Plugin not found.' => 'Plugin no encontrado.', + 'You don\'t have the permission to remove this plugin.' => 'No tiene permiso para eliminar este plugin.', + 'Unable to download plugin archive.' => 'No se puede descargar el plugin.', + 'Unable to write temporary file for plugin.' => 'No se puede escribir el fichero temporal del plugin.', + 'Unable to open plugin archive.' => 'No se puede abrir el plugin.', + 'There is no file in the plugin archive.' => 'No hay ficheros dentro del plugin.', + 'Create tasks in bulk' => 'Crear tareas en bloque', + 'Your Kanboard instance is not configured to install plugins from the user interface.' => 'Kanboard no está configurado para instalar plugins desde la interfaz de usuario.', + 'There is no plugin available.' => 'No hay plugins disponibles.', + 'Install' => 'Instalar', + 'Update' => 'Actualizar', + 'Up to date' => 'Actualizado', + 'Not available' => 'No disponible', + 'Remove plugin' => 'Eliminar plugin', + 'Do you really want to remove this plugin: "%s"?' => '¿Realmente desea eliminar este plugin: «%s»?', + 'Uninstall' => 'Desinstalar', + 'Listing' => 'Listado', + 'Metadata' => 'Metadatos', + 'Manage projects' => 'Administrar proyectos', + 'Convert to task' => 'Convertir en tarea', + 'Convert sub-task to task' => 'Convertir subtarea en tarea', + 'Do you really want to convert this sub-task to a task?' => '¿Realmente desea convertir esta subtarea en tarea?', + 'My task title' => 'Título de la tarea', + 'Enter one task by line.' => 'Introduce una tarea por línea.', + 'Number of failed login:' => 'Número de ingresos fallidos:', + 'Account locked until:' => 'Cuenta bloqueada hasta:', + 'Email settings' => 'Preferencias de correo electrónico', + 'Email sender address' => 'Dirección del remitente del correo electrónico', + 'Email transport' => 'Transporte de correo electrónico', + 'Webhook token' => 'Token del disparador web (webhook)', + 'Imports' => 'Importaciones', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/fi_FI/translations.php b/sources/app/Locale/fi_FI/translations.php index 8e5dd81..58360ab 100644 --- a/sources/app/Locale/fi_FI/translations.php +++ b/sources/app/Locale/fi_FI/translations.php @@ -35,7 +35,6 @@ return array( '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', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d suljettua tehtävää', 'No task for this project' => 'Ei tehtävää tälle projektille', 'Public link' => 'Julkinen linkki', - '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', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Lisää kuvaus', 'Comment added successfully.' => 'Kommentti lisättiin onnistuneesti.', 'Unable to create your comment.' => 'Kommentin lisäys epäonnistui.', - 'Edit this task' => 'Muokkaa tehtävää', 'Due Date' => 'Deadline', 'Invalid date' => 'Virheellinen päiväys', 'Automatic actions' => 'Automaattiset toiminnot', @@ -250,7 +246,6 @@ return array( '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.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Haluatko varmasti poistaa tiedoston: "%s"?', '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', @@ -370,7 +364,6 @@ return array( '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', @@ -413,9 +406,7 @@ return array( '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', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Poista kaista', '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.', @@ -517,7 +507,6 @@ return array( // 'All swimlanes' => '', // 'All colors' => '', // 'Moved to column %s' => '', - // 'Change description' => '', // 'User dashboard' => '', // 'Allow only one subtask in progress at the same time for a user' => '', // 'Edit column "%s"' => '', @@ -709,7 +698,6 @@ return array( // 'view the board on Kanboard' => '', // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', // 'New title: %s' => '', // 'The task is not assigned anymore' => '', // 'New assignee: %s' => '', @@ -738,7 +726,6 @@ return array( // 'Stop timer' => '', // 'Start timer' => '', // 'Add project member' => '', - // 'Enable notifications' => '', // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/fr_FR/translations.php b/sources/app/Locale/fr_FR/translations.php index cedd603..8684d1e 100644 --- a/sources/app/Locale/fr_FR/translations.php +++ b/sources/app/Locale/fr_FR/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Voir cette tâche', 'Remove user' => 'Supprimer un utilisateur', 'Do you really want to remove this user: "%s"?' => 'Voulez-vous vraiment supprimer cet utilisateur : « %s » ?', - 'New user' => 'Ajouter un utilisateur', 'All users' => 'Tous les utilisateurs', 'Username' => 'Nom d\'utilisateur', 'Password' => 'Mot de passe', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d tâches terminées', 'No task for this project' => 'Aucune tâche pour ce projet', 'Public link' => 'Lien public', - '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 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', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Laissez une description', 'Comment added successfully.' => 'Commentaire ajouté avec succès.', 'Unable to create your comment.' => 'Impossible de sauvegarder votre commentaire.', - 'Edit this task' => 'Modifier cette tâche', 'Due Date' => 'Date d\'échéance', 'Invalid date' => 'Date invalide', 'Automatic actions' => 'Actions automatisées', @@ -250,7 +246,6 @@ return array( 'Category' => 'Catégorie', 'Category:' => 'Catégorie :', 'Categories' => 'Catégories', - 'Category not found.' => 'Catégorie introuvable', 'Your category have been created successfully.' => 'Votre catégorie a été créée avec succès.', 'Unable to create your category.' => 'Impossible de créer votre catégorie.', 'Your category have been updated successfully.' => 'Votre catégorie a été mise à jour avec succès.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Voulez-vous vraiment supprimer ce fichier « %s » ?', 'Attachments' => 'Pièces-jointes', 'Edit the task' => 'Modifier la tâche', - 'Edit the description' => 'Modifier la description', 'Add a comment' => 'Ajouter un commentaire', 'Edit a comment' => 'Modifier un commentaire', 'Summary' => 'Résumé', @@ -367,10 +361,9 @@ return array( 'Password modification' => 'Changement de mot de passe', 'External authentications' => 'Authentifications externes', 'Never connected.' => 'Jamais connecté.', - 'No external authentication enabled.' => 'Aucune authentication externe activée.', + 'No external authentication enabled.' => 'Aucune authentification externe activée.', 'Password modified successfully.' => 'Mot de passe changé avec succès.', 'Unable to change the password.' => 'Impossible de changer le mot de passe.', - '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 %s' => '%s a mis à jour la tâche %s', '%s opened the task %s' => '%s a ouvert la tâche %s', @@ -413,9 +406,7 @@ return array( 'About' => 'À propos', 'Database driver:' => 'Type de base de données :', 'Board settings' => 'Paramètres du tableau', - 'URL and token' => 'URL et jeton de sécurité', 'Webhook settings' => 'Paramètres pour les webhooks', - 'URL for task creation:' => 'URL pour la création de tâche :', 'Reset token' => 'Regénérer le jeton de sécurité', 'API endpoint:' => 'URL de l\'API :', 'Refresh interval for private board' => 'Intervalle pour rafraîchir un tableau privé', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Supprimer une swimlane', '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.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Toutes les swimlanes', 'All colors' => 'Toutes les couleurs', '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 »', @@ -709,7 +698,7 @@ return array( 'view the board on Kanboard' => 'voir le tableau sur Kanboard', 'The task have been moved to the first swimlane' => 'La tâche a été déplacée dans la première swimlane', 'The task have been moved to another swimlane:' => 'La tâche a été déplacée dans une autre swimlane :', - 'Overdue tasks for the project "%s"' => 'Tâches en retard pour le projet « %s »', + // 'Overdue tasks for the project(s) "%s"' => 'Tâches en retard pour le projet « %s »', 'New title: %s' => 'Nouveau titre : %s', 'The task is not assigned anymore' => 'La tâche n\'est plus assignée maintenant', 'New assignee: %s' => 'Nouvel assigné : %s', @@ -738,7 +727,6 @@ return array( 'Stop timer' => 'Stopper le chrono', 'Start timer' => 'Démarrer le chrono', 'Add project member' => 'Ajouter un membre au projet', - 'Enable notifications' => 'Activer les notifications', 'My activity stream' => 'Mon flux d\'activité', 'My calendar' => 'Mon agenda', 'Search tasks' => 'Rechercher des tâches', @@ -1116,7 +1104,7 @@ return array( 'Column created successfully.' => 'La colonne a été créée avec succès.', 'Another column with the same name exists in the project' => 'Une autre colonne existe avec le même nom dans le projet', 'Default filters' => 'Filtres par défaut', - 'Your board doesn\'t have any column!' => 'Votre tableau n\'a aucune colonne', + 'Your board doesn\'t have any columns!' => 'Votre tableau n\'a aucune colonne', 'Change column position' => 'Changer la position de la colonne', 'Switch to the project overview' => 'Aller à l\'aperçu du projet', 'User filters' => 'Filtres des utilisateurs', @@ -1153,4 +1141,80 @@ return array( 'Upload my avatar image' => 'Uploader mon image d\'avatar', 'Remove my image' => 'Supprimer mon image', 'The OAuth2 state parameter is invalid' => 'Le paramètre "state" de OAuth2 est invalide', + 'User not found.' => 'Utilisateur introuvable.', + 'Search in activity stream' => 'Chercher dans le flux d\'activité', + 'My activities' => 'Mes activités', + 'Activity until yesterday' => 'Activités jusqu\'à hier', + 'Activity until today' => 'Activités jusqu\'à aujourd\'hui', + 'Search by creator: ' => 'Rechercher par créateur : ', + 'Search by creation date: ' => 'Rechercher par date de création : ', + 'Search by task status: ' => 'Rechercher par le statut des tâches : ', + 'Search by task title: ' => 'Rechercher par le titre des tâches : ', + 'Activity stream search' => 'Recherche dans le flux d\'activité', + 'Projects where "%s" is manager' => 'Projets où « %s » est gestionnaire', + 'Projects where "%s" is member' => 'Projets où « %s » est membre du projet', + 'Open tasks assigned to "%s"' => 'Tâches ouvertes assignées à « %s »', + 'Closed tasks assigned to "%s"' => 'Tâches fermées assignées à « %s »', + 'Assign automatically a color based on a priority' => 'Assigner automatiquement une couleur par rapport à une priorité', + 'Overdue tasks for the project(s) "%s"' => 'Tâches en retard pour le projet(s) « %s »', + 'Upload files' => 'Uploader les fichiers', + 'Installed Plugins' => 'Extensions installées', + 'Plugin Directory' => 'Liste des extensions', + 'Plugin installed successfully.' => 'Extension installée avec succès.', + 'Plugin updated successfully.' => 'Extension mise à jour avec succès.', + 'Plugin removed successfully.' => 'Extension supprimée avec succès.', + 'Subtask converted to task successfully.' => 'Sous-tâche convertie en tâche avec succès.', + 'Unable to convert the subtask.' => 'Impossible de convertir cette sous-tâche.', + 'Unable to extract plugin archive.' => 'Impossible d\'extraire l\'archive de l\'extension.', + 'Plugin not found.' => 'Extension introuvable.', + 'You don\'t have the permission to remove this plugin.' => 'Vous n\'avez pas la permission de supprimer ce plugin.', + 'Unable to download plugin archive.' => 'Impossible de télécharger l\'achive du plugin.', + 'Unable to write temporary file for plugin.' => 'Impossible d\'écrire le fichier temporaire pour l\'extension.', + 'Unable to open plugin archive.' => 'Impossible d\'ouvrir l\'archive du plugin.', + 'There is no file in the plugin archive.' => 'Il n\'y a aucun fichier dans l\'archive du plugin.', + 'Create tasks in bulk' => 'Créer plusieurs tâches en même temps', + 'Your Kanboard instance is not configured to install plugins from the user interface.' => 'Votre instance de Kanboard n\'est pas configurée pour installer des extension depuis l\'interface utilisateur.', + 'There is no plugin available.' => 'Il n\'a aucune extension disponible.', + 'Install' => 'Installer', + 'Update' => 'Mettre à jour', + 'Up to date' => 'À jour', + 'Not available' => 'Non disponible', + 'Remove plugin' => 'Supprimer l\'extension', + 'Do you really want to remove this plugin: "%s"?' => 'Voulez-vous vraiment supprimer cette extension : « %s » ?', + 'Uninstall' => 'Désinstaller', + 'Listing' => 'Listing', + 'Metadata' => 'Metadonnées', + 'Manage projects' => 'Gérer les projets', + 'Convert to task' => 'Convertir en tâche', + 'Convert sub-task to task' => 'Convertir une sous-tâche en tâche', + 'Do you really want to convert this sub-task to a task?' => 'Voulez-vous vraiment convertir cette sous-tâche en tâche ?', + 'My task title' => 'Mon titre pour la tâche', + 'Enter one task by line.' => 'Entrez une tâche par ligne.', + 'Number of failed login:' => 'Nombre de connexion échouées :', + 'Account locked until:' => 'Compte bloqué jusqu\'au :', + 'Email settings' => 'Paramètres des emails', + 'Email sender address' => 'Adresse email de l\'expéditeur', + 'Email transport' => 'Transport des emails', + 'Webhook token' => 'Jeton de sécurité des webhooks', + 'Imports' => 'Importations', + 'Project tags management' => 'Gestion des libellés pour le projet', + 'Tag created successfully.' => 'Libellé créé avec succès.', + 'Unable to create this tag.' => 'Imposssible de créer ce libellé.', + 'Tag updated successfully.' => 'Libellé mis à jour avec succès.', + 'Unable to update this tag.' => 'Impossible de mettre à jour ce libellé.', + 'Tag removed successfully.' => 'Libellé supprimé avec succès.', + 'Unable to remove this tag.' => 'Impossible de supprimer ce libellé.', + 'Global tags management' => 'Gestion des libellés globaux', + 'Tags' => 'Libellés', + 'Tags management' => 'Gestion des libellés', + 'Add new tag' => 'Ajouter un nouveau libellé', + 'Edit a tag' => 'Mettre à jour un libellé', + 'Project tags' => 'Libellés du projet', + 'There is no specific tag for this project at the moment.' => 'Il n\'y a aucun libellé spécifique pour ce projet pour le moment.', + 'Tag' => 'Libellé', + 'Remove a tag' => 'Supprimer un libellé', + 'Do you really want to remove this tag: "%s"?' => 'Voulez-vous vraiment supprimer ce libellé : « %s » ?', + 'Global tags' => 'Libellés globaux', + 'There is no global tag at the moment.' => 'Il n\'y a aucun libellé global pour le moment.', + 'This field cannot be empty' => 'Ce champ ne peut être vide', ); diff --git a/sources/app/Locale/hu_HU/translations.php b/sources/app/Locale/hu_HU/translations.php index f642a6c..c80c0a2 100644 --- a/sources/app/Locale/hu_HU/translations.php +++ b/sources/app/Locale/hu_HU/translations.php @@ -19,23 +19,22 @@ return array( 'Red' => 'Piros', 'Orange' => 'Narancs', 'Grey' => 'Szürke', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Brown' => 'Barna', + 'Deep Orange' => 'Sötét narancs', + 'Dark Grey' => 'Sötét szürke', + 'Pink' => 'Rózsaszín', + 'Teal' => 'Pávakék', + 'Cyan' => 'Ciánkék', + 'Lime' => 'Lime', + 'Light Green' => 'Világos zöld', + 'Amber' => 'Borostyán', '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ó', + 'Do you really want to remove this user: "%s"?' => 'Valóban törölni akarja ezt a felhasználót: "%s"?', 'All users' => 'Minden felhasználó', 'Username' => 'Felhasználónév', 'Password' => 'Jelszó', @@ -96,7 +95,7 @@ return array( '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"?', + 'Do you really want to open this task: "%s"?' => 'Valóban meg akarja nyitni ezt a feladatot: "%s"?', 'Back to the board' => 'Vissza a táblához', 'There is nobody assigned' => 'Nincs felelős', 'Column on the board:' => 'Tábla oszlopa: ', @@ -154,9 +153,7 @@ return array( 'Id' => 'ID', '%d closed tasks' => '%d lezárt feladat', 'No task for this project' => 'Nincs feladat ebben a projektben', - 'Public link' => 'Nyilvános link', - 'Change assignee' => 'Felelős módosítása', - 'Change assignee for the task "%s"' => 'Feladat felelősének módosítása: "%s"', + 'Public link' => 'Nyilvános hivatkozá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ó', @@ -170,7 +167,6 @@ return array( '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.', - 'Edit this task' => 'Feladat módosítása', 'Due Date' => 'Határidő', 'Invalid date' => 'Érvénytelen dátum', 'Automatic actions' => 'Automatikus intézkedések', @@ -203,7 +199,7 @@ return array( 'Position' => 'Pozíció', 'Duplicate to another project' => 'Másolás másik projektbe', 'Duplicate' => 'Másolás', - 'link' => 'link', + 'link' => 'hivatkozás', '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', @@ -235,10 +231,10 @@ return array( '%d comments' => '%d megjegyzés', '%d comment' => '%d megjegyzés', 'Email address invalid' => 'Érvénytelen e-mail cím', - // 'Your external account is not linked anymore to your profile.' => '', - // 'Unable to unlink your external account.' => '', - // 'External authentication failed' => '', - // 'Your external account is linked to your profile successfully.' => '', + 'Your external account is not linked anymore to your profile.' => 'Az ön külső számlája és az ön profilja közötti kapcsolat megszűnt.', + 'Unable to unlink your external account.' => 'Nem sikerült megszüntetni a kapcsolatot az ön külső számlájával.', + 'External authentication failed' => 'A külső jelszó ellenőrzés nem sikerült', + 'Your external account is linked to your profile successfully.' => 'Az ön külső számlája sikeresen össze lett kapcsolva az ön profiljával.', 'Email' => 'E-mail', 'Task removed successfully.' => 'Feladat sikeresen törölve.', 'Unable to remove this task.' => 'A feladatot nem lehet törölni.', @@ -250,7 +246,6 @@ return array( '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.', @@ -269,10 +264,9 @@ return array( '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"?', + 'Do you really want to remove this file: "%s"?' => 'Valóban törölni akarja ezt a fájlt: "%s"?', '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', @@ -343,8 +337,8 @@ return array( 'Disable public access' => 'Nyilvános hozzáférés letiltása', 'Enable public access' => 'Nyilvános hozzáférés engedélyezése', '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 enable this project: "%s"?' => 'Tényleg szeretné engedélyezni ezt a projektet: "%s"', + 'Do you really want to disable this project: "%s"?' => 'Tényleg szeretné letiltani 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', @@ -370,7 +364,6 @@ return array( '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', @@ -413,9 +406,7 @@ return array( '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', @@ -473,30 +464,29 @@ return array( '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', + 'Cumulative flow diagram' => 'Kumulatív folyamatábra', + 'Cumulative flow diagram for "%s"' => 'Kumulatív folyamatábra: %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.', - 'Active swimlanes' => 'Aktív folyamatok', - 'Add a new swimlane' => 'Új folyamat', - 'Change default swimlane' => 'Alapértelmezett folyamat változtatás', + 'Active swimlanes' => 'Aktív sávok', + 'Add a new swimlane' => 'Új sáv', + 'Change default swimlane' => 'Alapértelmezett sáv megváltoztatása', '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', - 'Remove a swimlane' => 'Folyamat törlé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 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.', + 'Do you really want to remove this swimlane: "%s"?' => 'Valóban törölni akarja ezt a sávot: %s ?', + 'Inactive swimlanes' => 'Inaktív sávok', + 'Remove a swimlane' => 'Sáv törlés', + 'Show default swimlane' => 'Alapértelmezett sáv megjelenítése', + 'Swimlane modification for the project "%s"' => '%s projekt sávjainak módosítása', + 'Swimlane removed successfully.' => 'Sáv sikeresen törölve.', + 'Swimlanes' => 'Sávok', + 'Swimlane updated successfully.' => 'Sáv sikeresen frissítve', + 'The default swimlane have been updated successfully.' => 'Az alapértelmezett sáv sikeresen frissítve.', + 'Unable to remove this swimlane.' => 'A sáv törlése sikertelen.', + 'Unable to update this swimlane.' => 'A sáv frissítése sikertelen.', + 'Your swimlane have been created successfully.' => 'A sáv 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)', 'Integrations' => 'Integráció', @@ -514,10 +504,9 @@ return array( 'Calendar' => 'Naptár', 'Next' => 'Következő', '#%d' => '#%d', - 'All swimlanes' => 'Minden folyamat', + 'All swimlanes' => 'Minden sáv', 'All colors' => 'Minden szín', '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', @@ -527,7 +516,7 @@ return array( '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?', - // 'Disallow login form' => '', + 'Disallow login form' => 'A login ablak letiltása', 'Start' => 'Kezdet', 'End' => 'Vég', 'Task age in days' => 'Feladat életkora napokban', @@ -544,7 +533,7 @@ return array( 'Link modification' => 'Hivatkozás módosítás', 'Links' => 'Hivatkozások', 'Link settings' => 'Hivatkozás beállítasok', - 'Opposite label' => 'Ellenekező címke', + 'Opposite label' => 'Ellenkező 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', @@ -585,7 +574,7 @@ return array( 'AUD - Australian Dollar' => 'AUD - Ausztrál dollár', 'CAD - Canadian Dollar' => 'CAD - Kanadai dollár', 'CHF - Swiss Francs' => 'CHF - Svájci frank', - 'Custom Stylesheet' => 'Egyéni sítluslap', + 'Custom Stylesheet' => 'Egyéni stíluslap', 'download' => 'letöltés', 'EUR - Euro' => 'EUR - Euro', 'GBP - British Pound' => 'GBP - Angol font', @@ -593,564 +582,638 @@ return array( 'JPY - Japanese Yen' => 'JPY - Japán Yen', 'NZD - New Zealand Dollar' => 'NZD - Új-Zélandi dollár', 'RSD - Serbian dinar' => 'RSD - Szerb dínár', - 'USD - US Dollar' => 'USD - Amerikai ollár', + 'USD - US Dollar' => 'USD - Amerikai dollár', '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', - // 'Transitions' => '', - // 'Executer' => '', - // 'Time spent in the column' => '', - // 'Task transitions' => '', - // 'Task transitions export' => '', - // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', - // 'Currency rates' => '', - // 'Rate' => '', - // 'Change reference currency' => '', - // 'Add a new currency rate' => '', - // 'Reference currency' => '', - // 'The currency rate have been added successfully.' => '', - // 'Unable to add this currency rate.' => '', - // 'Webhook URL' => '', - // '%s remove the assignee of the task %s' => '', - // 'Enable Gravatar images' => '', - // 'Information' => '', - // 'Check two factor authentication code' => '', - // 'The two factor authentication code is not valid.' => '', - // 'The two factor authentication code is valid.' => '', - // 'Code' => '', - // 'Two factor authentication' => '', - // 'This QR code contains the key URI: ' => '', - // 'Check my code' => '', - // 'Secret key: ' => '', - // 'Test your device' => '', - // 'Assign a color when the task is moved to a specific column' => '', - // '%s via Kanboard' => '', + 'Transitions' => 'Állapot-átmenetek', + 'Executer' => 'Végrehajtó', + 'Time spent in the column' => 'Az oszlopban töltött idő', + 'Task transitions' => 'Feladat állapot-átmenetek', + 'Task transitions export' => 'Feladat állapot-átmenetek exportálása', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Ez a riport az összes feladatra vonatkozóan tartalmazza az oszlop mozgatásokat. Szerepel benne a dátum, a felhasználó neve, és az egyes állapot-átemenetekkel eltöltött idő.', + 'Currency rates' => 'Árfolyamok', + 'Rate' => 'Árfolyam', + 'Change reference currency' => 'A bázis pénznem megváltoztatása', + 'Add a new currency rate' => 'Új átváltási árfolyam megadása', + 'Reference currency' => 'Bázis pénznem', + 'The currency rate have been added successfully.' => 'Az átváltási árfolyammal történő bővítés sikerült', + 'Unable to add this currency rate.' => 'Nem sikerült az átváltási árfolyam felvétele', + 'Webhook URL' => 'Webhook URL', + '%s remove the assignee of the task %s' => '%s eltávolította a %s feladathoz rendelt személyt', + 'Enable Gravatar images' => 'Gravatár képek engedélyezése', + 'Information' => 'Információ', + 'Check two factor authentication code' => 'Két fázisú beléptető kód ellenőrzése', + 'The two factor authentication code is not valid.' => 'A két fázisú beléptető kód érvénytelen', + 'The two factor authentication code is valid.' => 'A két fázisú beléptető kód érvényes', + 'Code' => 'Kód', + 'Two factor authentication' => 'Két fázisú beléptetés', + 'This QR code contains the key URI: ' => 'Ez a QR kód a következő kulcs URI-t tartalmazza: ', + 'Check my code' => 'A kódom ellenőrzése', + 'Secret key: ' => 'Titkos kulcs', + 'Test your device' => 'Az eszköz ellenőrzése', + 'Assign a color when the task is moved to a specific column' => 'Szín hozzárendelése, ha a feladatot egy adott oszlopba mozgatták', + '%s via Kanboard' => '%s a Kanboard-on keresztül', // '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' => '', - // 'Identifier' => '', - // 'Disable two factor authentication' => '', - // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', - // 'Edit link' => '', - // 'Start to type task title...' => '', - // 'A task cannot be linked to itself' => '', - // 'The exact same link already exists' => '', - // 'Recurrent task is scheduled to be generated' => '', - // 'Score' => '', - // 'The identifier must be unique' => '', - // 'This linked task id doesn\'t exists' => '', - // 'This value must be alphanumeric' => '', - // 'Edit recurrence' => '', - // 'Generate recurrent task' => '', - // 'Trigger to generate recurrent task' => '', - // 'Factor to calculate new due date' => '', - // 'Timeframe to calculate new due date' => '', - // 'Base date to calculate new due date' => '', - // 'Action date' => '', - // 'Base date to calculate new due date: ' => '', - // 'This task has created this child task: ' => '', - // 'Day(s)' => '', - // 'Existing due date' => '', - // 'Factor to calculate new due date: ' => '', - // 'Month(s)' => '', - // 'Recurrence' => '', - // 'This task has been created by: ' => '', - // 'Recurrent task has been generated:' => '', - // 'Timeframe to calculate new due date: ' => '', - // 'Trigger to generate recurrent task: ' => '', - // 'When task is closed' => '', - // 'When task is moved from first column' => '', - // 'When task is moved to last column' => '', - // 'Year(s)' => '', - // 'Calendar settings' => '', - // 'Project calendar view' => '', - // 'Project settings' => '', - // 'Show subtasks based on the time tracking' => '', - // 'Show tasks based on the creation date' => '', - // 'Show tasks based on the start date' => '', - // 'Subtasks time tracking' => '', - // 'User calendar view' => '', - // 'Automatically update the start date' => '', + 'This chart show the task complexity over the time (Work Remaining).' => 'Ez a diagram a feladat időbeli bonyolultságát ábrázolja (mennyi munka van hátra)', + 'Screenshot taken %s' => 'A képernyőmentés megtörtént, %s', + 'Add a screenshot' => 'Képernyőmentés hozzáadása', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Végezzen képernyőmentést, majd a CTRL+V vagy ⌘+V megnyomásával másolja be ide.', + 'Screenshot uploaded successfully.' => 'A képernyőmentés feltöltése sikeresen megtörtént.', + 'SEK - Swedish Krona' => 'SEK - Svéd korona', + 'Identifier' => 'Azonosító', + 'Disable two factor authentication' => 'A kétfázisú beléptetés letiltása', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Valóban le akarja tiltani a kétfázisú beléptetést ennél a felhasználónál: "%s"?', + 'Edit link' => 'Hivatkozás szerkesztése', + 'Start to type task title...' => 'Kezdje el begépelni a feladat címét... ', + 'A task cannot be linked to itself' => 'Egy feladatot nem lehet önmagához kapcsolni', + 'The exact same link already exists' => 'Már létezik pontosan ugyanez a hivatkozás', + 'Recurrent task is scheduled to be generated' => 'Az ismétlődő feladat előállítása ütemezve lett', + 'Score' => 'Pontszám', + 'The identifier must be unique' => 'Egyedi azonosító szükséges', + 'This linked task id doesn\'t exists' => 'Ez a hivatkozott feladat nem létezik', + 'This value must be alphanumeric' => 'Alfanumerikus érték szükséges', + 'Edit recurrence' => 'Ismétlődés szerkesztése', + 'Generate recurrent task' => 'Ismétlődő feladat előállítása', + 'Trigger to generate recurrent task' => 'Az ismétlődő feladatot előállító trigger', + 'Factor to calculate new due date' => 'Az új határidő kiszámításához használt tényező', + 'Timeframe to calculate new due date' => 'Az új határidő kiszámításához használt időablak', + 'Base date to calculate new due date' => 'A határidő kiszámításához használt kezdő dátum', + 'Action date' => 'Intézkedés dátuma', + 'Base date to calculate new due date: ' => 'Az új határidő kiszámításához használt kezdő dátum: ', + 'This task has created this child task: ' => 'Ez a feladat a következő leszármazott feladatot hozta létre: ', + 'Day(s)' => 'Nap(ok)', + 'Existing due date' => 'A jelenlegi határidő', + 'Factor to calculate new due date: ' => 'Az új határidő kiszámításához használt tényező: ', + 'Month(s)' => 'Hónap(ok)', + 'Recurrence' => 'Ismétlődés', + 'This task has been created by: ' => 'Ezt a feladatot a következő személy hozta létre: ', + 'Recurrent task has been generated:' => 'Ismétlődő feladat lett létrehozva: ', + 'Timeframe to calculate new due date: ' => 'Az új határidő kiszámításához használt időablak: ', + 'Trigger to generate recurrent task: ' => 'Az ismétlődő feladatot előállító trigger: ', + 'When task is closed' => 'Mikor a feladat be lett zárva', + 'When task is moved from first column' => 'Mikor a feladat az első oszlopból el lett mozgatva', + 'When task is moved to last column' => 'Mikor a feladat az utolsó oszlopba lett elmozgatva', + 'Year(s)' => 'Év(ek)', + 'Calendar settings' => 'Naptár beállítások', + 'Project calendar view' => 'A projekt megjelenítése naptári formában', + 'Project settings' => 'Projekt beállítások', + 'Show subtasks based on the time tracking' => 'A részfeladatok megjelenítése az idő nyomkövetés alapján', + 'Show tasks based on the creation date' => 'A feladatok megjelenítése a létrehozás dátuma alapján', + 'Show tasks based on the start date' => 'A feladatok megjelenítése a kezdő dátum alapján', + 'Subtasks time tracking' => 'A részfeladatok idejének megjelenítése', + 'User calendar view' => 'A felhasználó naptárának megjelenítése', + 'Automatically update the start date' => 'A kezdő dátum automatikus módosítása', // 'iCal feed' => '', - // 'Preferences' => '', - // 'Security' => '', - // 'Two factor authentication disabled' => '', - // 'Two factor authentication enabled' => '', - // 'Unable to update this user.' => '', - // 'There is no user management for private projects.' => '', - // 'User that will receive the email' => '', - // 'Email subject' => '', - // 'Date' => '', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Column change' => '', - // 'Position change' => '', - // 'Swimlane change' => '', - // 'Assignee change' => '', - // '[%s] Overdue tasks' => '', - // 'Notification' => '', - // '%s moved the task #%d to the first swimlane' => '', - // '%s moved the task #%d to the swimlane "%s"' => '', - // 'Swimlane' => '', - // 'Gravatar' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', - // 'This report contains all subtasks information for the given date range.' => '', - // 'This report contains all tasks information for the given date range.' => '', - // 'Project activities for %s' => '', - // 'view the board on Kanboard' => '', - // 'The task have been moved to the first swimlane' => '', - // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', - // 'The task is not assigned anymore' => '', - // 'New assignee: %s' => '', - // 'There is no category now' => '', - // 'New category: %s' => '', - // 'New color: %s' => '', - // 'New complexity: %d' => '', - // 'The due date have been removed' => '', - // 'There is no description anymore' => '', - // 'Recurrence settings have been modified' => '', - // 'Time spent changed: %sh' => '', - // 'Time estimated changed: %sh' => '', - // 'The field "%s" have been updated' => '', - // 'The description has been modified:' => '', - // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', - // 'Only for tasks assigned to me' => '', - // 'Only for tasks created by me' => '', - // 'Only for tasks created by me and assigned to me' => '', - // '%%Y-%%m-%%d' => '', - // 'Total for all columns' => '', - // 'You need at least 2 days of data to show the chart.' => '', - // '<15m' => '', - // '<30m' => '', - // 'Stop timer' => '', - // 'Start timer' => '', - // 'Add project member' => '', - // 'Enable notifications' => '', - // 'My activity stream' => '', - // 'My calendar' => '', - // 'Search tasks' => '', - // 'Reset filters' => '', - // 'My tasks due tomorrow' => '', - // 'Tasks due today' => '', - // 'Tasks due tomorrow' => '', - // 'Tasks due yesterday' => '', - // 'Closed tasks' => '', - // 'Open tasks' => '', - // 'Not assigned' => '', - // 'View advanced search syntax' => '', - // 'Overview' => '', - // 'Board/Calendar/List view' => '', - // 'Switch to the board view' => '', - // 'Switch to the calendar view' => '', - // 'Switch to the list view' => '', - // 'Go to the search/filter box' => '', - // 'There is no activity yet.' => '', - // 'No tasks found.' => '', - // 'Keyboard shortcut: "%s"' => '', - // 'List' => '', - // 'Filter' => '', - // 'Advanced search' => '', - // 'Example of query: ' => '', - // 'Search by project: ' => '', - // 'Search by column: ' => '', - // 'Search by assignee: ' => '', - // 'Search by color: ' => '', - // 'Search by category: ' => '', - // 'Search by description: ' => '', - // 'Search by due date: ' => '', - // 'Lead and Cycle time for "%s"' => '', - // 'Average time spent into each column for "%s"' => '', - // 'Average time spent into each column' => '', - // 'Average time spent' => '', - // 'This chart show the average time spent into each column for the last %d tasks.' => '', - // 'Average Lead and Cycle time' => '', - // 'Average lead time: ' => '', - // 'Average cycle time: ' => '', - // 'Cycle Time' => '', - // 'Lead Time' => '', - // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', - // 'Average time into each column' => '', - // 'Lead and cycle time' => '', - // 'Lead time: ' => '', - // 'Cycle time: ' => '', - // 'Time spent into each column' => '', - // 'The lead time is the duration between the task creation and the completion.' => '', - // 'The cycle time is the duration between the start date and the completion.' => '', - // 'If the task is not closed the current time is used instead of the completion date.' => '', - // 'Set automatically the start date' => '', - // 'Edit Authentication' => '', - // 'Remote user' => '', - // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', - // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', - // 'New remote user' => '', - // 'New local user' => '', - // 'Default task color' => '', - // 'This feature does not work with all browsers.' => '', - // 'There is no destination project available.' => '', - // 'Trigger automatically subtask time tracking' => '', - // 'Include closed tasks in the cumulative flow diagram' => '', - // 'Current swimlane: %s' => '', - // 'Current column: %s' => '', - // 'Current category: %s' => '', - // 'no category' => '', - // 'Current assignee: %s' => '', - // 'not assigned' => '', - // 'Author:' => '', - // 'contributors' => '', - // 'License:' => '', - // 'License' => '', - // 'Enter the text below' => '', - // 'Gantt chart for %s' => '', - // 'Sort by position' => '', - // 'Sort by date' => '', - // 'Add task' => '', - // 'Start date:' => '', - // 'Due date:' => '', - // 'There is no start date or due date for this task.' => '', - // 'Moving or resizing a task will change the start and due date of the task.' => '', - // 'There is no task in your project.' => '', - // 'Gantt chart' => '', - // 'People who are project managers' => '', - // 'People who are project members' => '', - // 'NOK - Norwegian Krone' => '', - // 'Show this column' => '', - // 'Hide this column' => '', - // 'open file' => '', - // 'End date' => '', - // 'Users overview' => '', - // 'Members' => '', - // 'Shared project' => '', - // 'Project managers' => '', - // 'Gantt chart for all projects' => '', - // 'Projects list' => '', - // 'Gantt chart for this project' => '', - // 'Project board' => '', - // 'End date:' => '', - // 'There is no start date or end date for this project.' => '', - // 'Projects Gantt chart' => '', - // 'Change task color when using a specific task link' => '', - // 'Task link creation or modification' => '', - // 'Milestone' => '', - // 'Documentation: %s' => '', - // 'Switch to the Gantt chart view' => '', - // 'Reset the search/filter box' => '', - // 'Documentation' => '', - // 'Table of contents' => '', - // 'Gantt' => '', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', - // 'Set maximum column height' => '', - // 'Remove maximum column height' => '', - // 'My notifications' => '', - // 'Custom filters' => '', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', - // 'Web' => '', - // 'New attachment on task #%d: %s' => '', - // 'New comment on task #%d' => '', - // 'Comment updated on task #%d' => '', - // 'New subtask on task #%d' => '', - // 'Subtask updated on task #%d' => '', - // 'New task #%d: %s' => '', - // 'Task updated #%d' => '', - // 'Task #%d closed' => '', - // 'Task #%d opened' => '', - // 'Column changed for task #%d' => '', - // 'New position for task #%d' => '', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', - // 'No new notifications.' => '', - // 'Mark all as read' => '', - // 'Mark as read' => '', - // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', - // 'Shared' => '', - // 'Owner' => '', - // 'Unread notifications' => '', - // 'Notification methods:' => '', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', + 'Preferences' => 'Preferenciák', + 'Security' => 'Biztonság', + 'Two factor authentication disabled' => 'A két fázisú beléptetés tiltva van', + 'Two factor authentication enabled' => 'A két fázisú beléptetés engedélyezve van', + 'Unable to update this user.' => 'A felhasználó {adatainak} módosítása nem sikerült.', + 'There is no user management for private projects.' => 'A privát projektek esetén nincs felhasználó kezelés', + 'User that will receive the email' => 'Az email-t a következő felhaszáló fogja megkapni', + 'Email subject' => 'Email tárgy', + 'Date' => 'Dátum', + 'Add a comment log when moving the task between columns' => 'Egy napló megjegyzés létrehozása a feladat oszlopok közötti mozgatásakor', + 'Move the task to another column when the category is changed' => 'A feladat átmozgatása egy másik oszlopba, ha megváltozik a kategória', + 'Send a task by email to someone' => 'Email-en egy feladat küldése valakinek', + 'Reopen a task' => 'Egy feladat újbóli megnyitása', + 'Column change' => 'Oszlop módosítás', + 'Position change' => 'Helyzet módosítás', + 'Swimlane change' => 'Sáv módosítás', + 'Assignee change' => 'Felelős módosítása', + '[%s] Overdue tasks' => '[%s] késésben lévő feladat', + 'Notification' => 'Értesítés', + '%s moved the task #%d to the first swimlane' => '%s a #%d feladatot az első sávba mozgatta', + '%s moved the task #%d to the swimlane "%s"' => '%s a #%d feladatot a "%s" sávba mozgatta', + 'Swimlane' => 'Sáv', + 'Gravatar' => 'Gravatár', + '%s moved the task %s to the first swimlane' => '%s a %s feladatot az első sávba mozgatta', + '%s moved the task %s to the swimlane "%s"' => '%s a %s feladatot a "%s" sávba mozgatta', + 'This report contains all subtasks information for the given date range.' => 'Ez a riport az adott dátumtartományra vonatkozón az összes részfeladatot tartalmazza', + 'This report contains all tasks information for the given date range.' => 'Ez a riport az adott dátumtartományra vonatkozóan az összes feladatot tartalmazza', + 'Project activities for %s' => '%s projekt tevékenységei', + 'view the board on Kanboard' => 'a tábla megjelenítése a Kanboard-on', + 'The task have been moved to the first swimlane' => 'A feladat ez első sávba lett elmozgatva', + 'The task have been moved to another swimlane:' => 'A feladat egy másik sávba lett elmozgatva', + 'New title: %s' => 'Új cím: %s', + 'The task is not assigned anymore' => 'A feladatnak már nincs felelőse', + 'New assignee: %s' => 'Az új felelős: %s', + 'There is no category now' => 'Jelenleg nincs kategória', + 'New category: %s' => 'Az új kategória: %s', + 'New color: %s' => 'Az új szín: %s', + 'New complexity: %d' => 'Az új bonyolultság: %d', + 'The due date have been removed' => 'A határidő törölve lett', + 'There is no description anymore' => 'Többé már nincs leírás', + 'Recurrence settings have been modified' => 'Az ismétlődés beállításai módosultak', + 'Time spent changed: %sh' => 'Módosult a ráfordított idő: %s óra', + 'Time estimated changed: %sh' => 'Módosult az időbecslés: %s óra', + 'The field "%s" have been updated' => 'A "%s" mező módosítva lett', + 'The description has been modified:' => 'A leírás módosítva lett:', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Valóban be akarja zárni a "%s" feladatot, valamint a hozzá tartozó részfeladatokat?', + 'I want to receive notifications for:' => 'Értesítéseket szeretnék kapni a következőkről:', + 'All tasks' => 'Az összes feladat', + 'Only for tasks assigned to me' => 'Csak a hozzám rendelt feladatok', + 'Only for tasks created by me' => 'Csak az általam létrehozott feladatok', + 'Only for tasks created by me and assigned to me' => 'Csak az általam lérehozott és a hozzám rendelt feladatok', + '%%Y-%%m-%%d' => '%%Y-%%m-%%d', + 'Total for all columns' => 'Az összes oszlop összege', + 'You need at least 2 days of data to show the chart.' => 'Legalább 2 nap adatára van szükség az ábra megjelenítéséshez', + '<15m' => '15p', + '<30m' => '30p', + 'Stop timer' => 'Időmérő leállítása', + 'Start timer' => 'Időmérő elindítása', + 'Add project member' => 'Projekt tag hozzáadása', + 'My activity stream' => 'Tevékenységem', + 'My calendar' => 'Naptáram', + 'Search tasks' => 'Feladatok közötti keresés', + 'Reset filters' => 'Szűrő alaphelyzetbe állítás', + 'My tasks due tomorrow' => 'Holnapi határidejű feladataim', + 'Tasks due today' => 'Mai határidejű feladatok', + 'Tasks due tomorrow' => 'Holnapi határidejű feladatok', + 'Tasks due yesterday' => 'Tegnapi határidejű feladatok', + 'Closed tasks' => 'Lezárt feladatok', + 'Open tasks' => 'Nyitott feladatok', + 'Not assigned' => 'Nincs felelős', + 'View advanced search syntax' => 'Részletes keresés megjelenítése', + 'Overview' => 'Áttekintés', + 'Board/Calendar/List view' => 'Tábla/Naptár/Lista nézet', + 'Switch to the board view' => 'Átkapcsolás tábla nézetbe', + 'Switch to the calendar view' => 'Átkapcsolás naptár nézetbe', + 'Switch to the list view' => 'Átkapcsolás lista nézetbe', + 'Go to the search/filter box' => 'Ugrás a keresés/szűrés dobozhoz', + 'There is no activity yet.' => 'Még nincs tevékenység', + 'No tasks found.' => 'Nincs feladat.', + 'Keyboard shortcut: "%s"' => 'Billentyű parancsok: "%s"', + 'List' => 'Lista', + 'Filter' => 'Szűrő', + 'Advanced search' => 'Keresés haladóknak', + 'Example of query: ' => 'Lekérdezési példa: ', + 'Search by project: ' => 'Keresés projekt alapján: ', + 'Search by column: ' => 'Keresés oszlop alapján: ', + 'Search by assignee: ' => 'Keresés felelős alapján: ', + 'Search by color: ' => 'Keresés szín alapján: ', + 'Search by category: ' => 'Keresés kategória alapján: ', + 'Search by description: ' => 'Keresés leírás alapján: ', + 'Search by due date: ' => 'Keresés határidő alapján: ', + 'Lead and Cycle time for "%s"' => 'A "%s" átfutási ideje és ciklusideje', + 'Average time spent into each column for "%s"' => 'A "%s" során az egyes oszlopokban töltött átlagos idő', + 'Average time spent into each column' => 'Az egyes oszlopokban töltött átlagos idő', + 'Average time spent' => 'Az eltöltött átlagos idő', + 'This chart show the average time spent into each column for the last %d tasks.' => 'Ez az ábra az utolsó %d feladatra vonatkozóan mutatja az egyes oszlopkban eltöltött átlagos időt.', + 'Average Lead and Cycle time' => 'Átlagos átfutási idő és ciklusidő', + 'Average lead time: ' => 'Átlagos átfutási idő: ', + 'Average cycle time: ' => 'Átlagos ciklusidő: ', + 'Cycle Time' => 'Ciklusidő', + 'Lead Time' => 'Átfutási idő', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Ez az ábra az utolsó %d feladatra vonatkozóan mutatja az átlagos átfutási időt és ciklusidőt az idő függvényében.', + 'Average time into each column' => 'Az egyes oszlopokban töltött átlagos idő', + 'Lead and cycle time' => 'Átfutási és ciklusidő', + 'Lead time: ' => 'Átfutási idő: ', + 'Cycle time: ' => 'Ciklusidő: ', + 'Time spent into each column' => 'Az egyes oszlopokban töltött idő', + 'The lead time is the duration between the task creation and the completion.' => 'Az átfutási idő a feladat létrehozása és befejezése között eltelt idő.', + 'The cycle time is the duration between the start date and the completion.' => 'A ciklusidő a feladat elkezdése és befejezése közötti eltelt idő.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Ha a feladat még nincs lezárva, akkor befejezés ideje helyett az aktuális idő lesz használva.', + 'Set automatically the start date' => 'A kezdési idő automatikus beállítása', + 'Edit Authentication' => 'A beléptetés szerkesztése', + 'Remote user' => 'Távoli felhasználó', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'A távoli felhasználók jelszava nem a Kanboard adatbázisban van tárolva. Példák: LDAP, Google és GitHub számlák.', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Ha bekattintja a "Bejelentkezési ablak tiltása" jelölőnégyzetet, akkor a login ablakban megadott jelszó nem lesz figyelembe véve.', + 'New remote user' => 'Új távoli felhasználó', + 'New local user' => 'Új helyi felhasználó', + 'Default task color' => 'A feladathoz rendelt alapszín', + 'This feature does not work with all browsers.' => 'Ez a jellemző nem minden böngészőben működik.', + 'There is no destination project available.' => 'Nincs ilyen cél projekt.', + 'Trigger automatically subtask time tracking' => 'A részfeladatok időfelhasználas-követésének automatikus indítása', + 'Include closed tasks in the cumulative flow diagram' => 'A lezárt feladatok szerepeltetáse az összesített folyamatábrán', + 'Current swimlane: %s' => 'Aktuális sáv: %s', + 'Current column: %s' => 'Aktuális oszlop: %s', + 'Current category: %s' => 'Aktuális kategória: %s', + 'no category' => 'nincs kategória', + 'Current assignee: %s' => 'Aktuális felelős: %s', + 'not assigned' => 'nincs kijelölt felelős', + 'Author:' => 'Szerző:', + 'contributors' => 'bedolgozók', + 'License:' => 'Engedély:', + 'License' => 'Engedély', + 'Enter the text below' => 'Adja be a lenti szöveget', + 'Gantt chart for %s' => 'Gantt diagram a %s számára', + 'Sort by position' => 'Rendezés hely szerint', + 'Sort by date' => 'Rendezés idő szerint', + 'Add task' => 'Feladat hozzáadása', + 'Start date:' => 'Kezdés ideje: ', + 'Due date:' => 'Határidő: ', + 'There is no start date or due date for this task.' => 'Ehhez a feladathoz nem adtak meg kezdési időt vagy határidőt.', + 'Moving or resizing a task will change the start and due date of the task.' => 'A feladat elmozgatása vagy méretének megváltoztatása meg fogja változtatni a feladat kezdési idejét és határidejét.', + 'There is no task in your project.' => 'Az ön projektjében nincsenek feladatok.', + 'Gantt chart' => 'Gantt diagram', + 'People who are project managers' => 'Projekt vezetők', + 'People who are project members' => 'Projekt tagok', + 'NOK - Norwegian Krone' => 'NOK - Norvég korona', + 'Show this column' => 'Oszlop mutatása', + 'Hide this column' => 'Oszlop elrejtése', + 'open file' => 'fájl megnyitás', + 'End date' => 'Végdátum', + 'Users overview' => 'Felhasználók áttekintése', + 'Members' => 'Tagok', + 'Shared project' => 'Közös projekt', + 'Project managers' => 'Projektvezetők', + 'Gantt chart for all projects' => 'Gantt diagram az összes projektre vonatkozóan', + 'Projects list' => 'Projekt lista', + 'Gantt chart for this project' => 'Gantt diagram erre a projektre vonatkozóan', + 'Project board' => 'Projekt tábla', + 'End date:' => 'Befejezés dátuma: ', + 'There is no start date or end date for this project.' => 'Ennél a projektnél nem adták meg a kezdés és a befejezés dátumát.', + 'Projects Gantt chart' => 'A projektek Gantt diagramja', + 'Change task color when using a specific task link' => 'Az egyes specifikus feladat hivatkozások használatakor a feladat színének megváltoztatása', + 'Task link creation or modification' => 'Feladat hivatkozás létrehozása vagy módosítása', + 'Milestone' => 'Mérföldkő', + 'Documentation: %s' => 'Dokumentáció: %s', + 'Switch to the Gantt chart view' => 'Átkapcsolás a Gantt diagram nézetre', + 'Reset the search/filter box' => 'A keresés/szűrés doboz alaphelyzetbe állítása', + 'Documentation' => 'Dokumentáció', + 'Table of contents' => 'Tartalomjegyzék', + 'Gantt' => 'Gantt', + 'Author' => 'Szerző', + 'Version' => 'Verzió', + 'Plugins' => 'Plugin-ek', + 'There is no plugin loaded.' => 'Nincs betöltött plugin.', + 'Set maximum column height' => 'Max. oszlopmagasság beállítása', + 'Remove maximum column height' => 'Max. oszlopmagasság törlése', + 'My notifications' => 'Emlékeztetőim', + 'Custom filters' => 'Egyedi szűrők', + 'Your custom filter have been created successfully.' => 'Az ön egyedi szűrője sikeresen létrejött.', + 'Unable to create your custom filter.' => 'Nem sikerült létrehozni az ön egyedi szűrőjét.', + 'Custom filter removed successfully.' => 'Az egyedi szűrő sikeresen törölve lett.', + 'Unable to remove this custom filter.' => 'Nem sikerült egy egyedi szűrő törlése.', + 'Edit custom filter' => 'Egyedi szűrő szerkesztése', + 'Your custom filter have been updated successfully.' => 'Az ön egyedi szűrője sikeresen módosult.', + 'Unable to update custom filter.' => 'Nem sikerült az egyedi szűrő módosítása', + 'Web' => 'Web (háló)', + 'New attachment on task #%d: %s' => 'A #%d számú feladatnak új melléklete van: %s', + 'New comment on task #%d' => 'A #%d számú feladatnak új megjegyzése van', + 'Comment updated on task #%d' => 'A #%d szűmú feladathoz tartozó megjegyzés módosult', + 'New subtask on task #%d' => 'A #%d számú feladatnak új részfeladata van', + 'Subtask updated on task #%d' => 'A #%d számú feladat részfeladata módosult', + 'New task #%d: %s' => 'Új #%d számú feladat: %s', + 'Task updated #%d' => 'A #%d számú feladat módosult', + 'Task #%d closed' => 'A #%d számú feladat le lett zárva', + 'Task #%d opened' => 'A #%d számú feladat meg lett nyitva', + 'Column changed for task #%d' => 'A #%d számú feladat oszlopa módosult', + 'New position for task #%d' => 'A #%d számú feladat új helyre került', + 'Swimlane changed for task #%d' => 'A #%d számú feladat új sávba került', + 'Assignee changed on task #%d' => 'A #%d számú feladat felelőse megváltozott', + '%d overdue tasks' => '%d db feladatnál van határidő túllépés', + 'Task #%d is overdue' => 'A #%d számú feladat határideje lejárt', + 'No new notifications.' => 'Nincs új emlékeztető.', + 'Mark all as read' => 'Az összes megjelölése olvasottként', + 'Mark as read' => 'Megjelölés olvasottként', + 'Total number of tasks in this column across all swimlanes' => 'Az ebben az oszlopban, az összes sávban lévő feladatok száma', + 'Collapse swimlane' => 'Sáv összecsukása', + 'Expand swimlane' => 'Sáv lenyitása', + 'Add a new filter' => 'Új szűrő hozzáadása', + 'Share with all project members' => 'Megosztás minden projekt taggal', + 'Shared' => 'Megosztva', + 'Owner' => 'Tulajdonos', + 'Unread notifications' => 'Olvasatlan értesítések', + 'Notification methods:' => 'Értesítési módszerek:', + 'Import tasks from CSV file' => 'Feladatok beolvasása CSV fájlból', + 'Unable to read your file' => 'A fájl nem olvasható', + '%d task(s) have been imported successfully.' => '%d feladat sikeresen feldolgozva.', + 'Nothing have been imported!' => 'Nem történt beolvasás!', + 'Import users from CSV file' => 'Felhasználók importálása CSV fájlból', + '%d user(s) have been imported successfully.' => '%d felhasználó sikeresen importálva.', // 'Comma' => '', // 'Semi-colon' => '', // 'Tab' => '', // 'Vertical bar' => '', // 'Double Quote' => '', // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', - // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'Import' => '', - // 'change sorting' => '', - // 'Tasks Importation' => '', + '%s attached a file to the task #%d' => '%s hozzákapcsolt a #%d feladathoz egy fájlt', + 'There is no column or swimlane activated in your project!' => 'Az ön projektjében nincs aktív oszlop vagy sáv!', + 'Append filter (instead of replacement)' => 'Szűrő hozzáfűzése (a helyettesítés helyett)', + 'Append/Replace' => 'Hozzáfűzés/Helyettesítés', + 'Append' => 'Hozzáfűz', + 'Replace' => 'Helyettesít', + 'Import' => 'Importál', + 'change sorting' => 'rendezési sorrend megváltoztatása', + 'Tasks Importation' => 'Feladat importálás', // 'Delimiter' => '', // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', - // 'No external integration registered.' => '', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', - // 'Link type' => '', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', - // 'Manager' => '', - // 'Project Manager' => '', - // 'Project Member' => '', - // 'Project Viewer' => '', - // 'Your account is locked for %d minutes' => '', - // 'Invalid captcha' => '', - // 'The name must be unique' => '', - // 'View all groups' => '', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', - // 'Add group member' => '', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', - // 'Permissions' => '', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', - // 'Role' => '', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', - // 'Role:' => '', - // 'Project members' => '', - // 'Compare hours for "%s"' => '', - // '%s mentioned you in the task #%d' => '', - // '%s mentioned you in a comment on the task #%d' => '', - // 'You were mentioned in the task #%d' => '', - // 'You were mentioned in a comment on the task #%d' => '', - // 'Mentioned' => '', - // 'Compare Estimated Time vs Actual Time' => '', - // 'Estimated hours: ' => '', - // 'Actual hours: ' => '', - // 'Hours Spent' => '', - // 'Hours Estimated' => '', - // 'Estimated Time' => '', - // 'Actual Time' => '', - // 'Estimated vs actual time' => '', - // 'RUB - Russian Ruble' => '', - // 'Assign the task to the person who does the action when the column is changed' => '', - // 'Close a task in a specific column' => '', - // 'Time-based One-time Password Algorithm' => '', - // 'Two-Factor Provider: ' => '', - // 'Disable two-factor authentication' => '', - // 'Enable two-factor authentication' => '', - // 'There is no integration registered at the moment.' => '', - // 'Password Reset for Kanboard' => '', - // 'Forgot password?' => '', - // 'Enable "Forget Password"' => '', - // 'Password Reset' => '', - // 'New password' => '', - // 'Change Password' => '', - // 'To reset your password click on this link:' => '', - // 'Last Password Reset' => '', - // 'The password has never been reinitialized.' => '', - // 'Creation' => '', - // 'Expiration' => '', - // 'Password reset history' => '', - // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', - // 'Do you really want to close all tasks of this column?' => '', - // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', - // 'Close all tasks of this column' => '', - // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', - // 'My dashboard' => '', - // 'My profile' => '', - // 'Project owner: ' => '', - // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', - // 'Project owner' => '', - // 'Those dates are useful for the project Gantt chart.' => '', - // 'Private projects do not have users and groups management.' => '', - // 'There is no project member.' => '', - // 'Priority' => '', - // 'Task priority' => '', - // 'General' => '', - // 'Dates' => '', - // 'Default priority' => '', - // 'Lowest priority' => '', - // 'Highest priority' => '', - // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Close a task when there is no activity' => '', - // 'Duration in days' => '', - // 'Send email when there is no activity on a task' => '', - // 'Unable to fetch link information.' => '', - // 'Daily background job for tasks' => '', - // 'Auto' => '', - // 'Related' => '', - // 'Attachment' => '', - // 'Title not found' => '', - // 'Web Link' => '', - // 'External links' => '', - // 'Add external link' => '', - // 'Type' => '', - // 'Dependency' => '', - // 'Add internal link' => '', - // 'Add a new external link' => '', - // 'Edit external link' => '', - // 'External link' => '', - // 'Copy and paste your link here...' => '', - // 'URL' => '', - // 'Internal links' => '', - // 'Assign to me' => '', - // 'Me' => '', - // 'Do not duplicate anything' => '', - // 'Projects management' => '', - // 'Users management' => '', - // 'Groups management' => '', - // 'Create from another project' => '', - // 'open' => '', - // 'closed' => '', - // 'Priority:' => '', - // 'Reference:' => '', - // 'Complexity:' => '', - // 'Swimlane:' => '', - // 'Column:' => '', - // 'Position:' => '', - // 'Creator:' => '', - // 'Time estimated:' => '', - // '%s hours' => '', - // 'Time spent:' => '', - // 'Created:' => '', - // 'Modified:' => '', - // 'Completed:' => '', - // 'Started:' => '', - // 'Moved:' => '', - // 'Task #%d' => '', - // 'Date and time format' => '', - // 'Time format' => '', - // 'Start date: ' => '', - // 'End date: ' => '', - // 'New due date: ' => '', - // 'Start date changed: ' => '', - // 'Disable private projects' => '', - // 'Do you really want to remove this custom filter: "%s"?' => '', - // 'Remove a custom filter' => '', - // 'User activated successfully.' => '', - // 'Unable to enable this user.' => '', - // 'User disabled successfully.' => '', - // 'Unable to disable this user.' => '', - // 'All files have been uploaded successfully.' => '', - // 'View uploaded files' => '', - // 'The maximum allowed file size is %sB.' => '', - // 'Choose files again' => '', - // 'Drag and drop your files here' => '', - // 'choose files' => '', - // 'View profile' => '', - // 'Two Factor' => '', - // 'Disable user' => '', - // 'Do you really want to disable this user: "%s"?' => '', - // 'Enable user' => '', - // 'Do you really want to enable this user: "%s"?' => '', - // 'Download' => '', - // 'Uploaded: %s' => '', - // 'Size: %s' => '', - // 'Uploaded by %s' => '', - // 'Filename' => '', - // 'Size' => '', - // 'Column created successfully.' => '', - // 'Another column with the same name exists in the project' => '', - // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', - // 'Change column position' => '', - // 'Switch to the project overview' => '', - // 'User filters' => '', - // 'Category filters' => '', - // 'Upload a file' => '', - // 'View file' => '', - // 'Last activity' => '', - // 'Change subtask position' => '', - // 'This value must be greater than %d' => '', - // 'Another swimlane with the same name exists in the project' => '', - // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', - // 'Actions duplicated successfully.' => '', - // 'Unable to duplicate actions.' => '', - // 'Add a new action' => '', - // 'Import from another project' => '', - // 'There is no action at the moment.' => '', - // 'Import actions from another project' => '', - // 'There is no available project.' => '', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', - // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', - // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', - // 'Menu' => '', - // 'Set start date' => '', - // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'CSV File' => 'CSV fájl', + 'Instructions' => 'Utasítások', + 'Your file must use the predefined CSV format' => 'A fájlnak az előre definiált CSV formátumot kell használnia', + 'Your file must be encoded in UTF-8' => 'A fájlnak UTF-8 karakterkódolást kell használnia', + 'The first row must be the header' => 'Az első sor kötelezően a fejléc', + 'Duplicates are not verified for you' => 'Az ismétlődések nincsenek ellenőrizve.', + 'The due date must use the ISO format: YYYY-MM-DD' => 'A határidőt ISO formátumban kell megadni: YYYY-MM-DD', + 'Download CSV template' => 'A CSV minta (template) letöltése', + 'No external integration registered.' => 'Nincs külső integráció regisztrálva.', + 'Duplicates are not imported' => 'Az ismétlődő értékek nem lesznek beimportálva', + 'Usernames must be lowercase and unique' => 'A felhasználói nevekre kötelező, hogy kisbetűsek és egyediek legyenek', + 'Passwords will be encrypted if present' => 'A jelszavak titkosítva lesznek', + '%s attached a new file to the task %s' => '%s a %s feladathoz egy új fájlt kapcsolt hozzá', + 'Link type' => 'Hivatkozás típus', + 'Assign automatically a category based on a link' => 'A hivatkozás alapján kategória automatikus hozzárendelése', + 'BAM - Konvertible Mark' => 'BAM - Konvertibilis márka', + 'Assignee Username' => 'A felelős felhasználói neve', + 'Assignee Name' => 'A felelős neve', + 'Groups' => 'Csoportok', + 'Members of %s' => 'A %s tagjai', + 'New group' => 'Új csoport', + 'Group created successfully.' => 'A csoport sikeresen létrejött.', + 'Unable to create your group.' => 'Nem sikerült a csoport létrehozása.', + 'Edit group' => 'Csoport szerkesztése', + 'Group updated successfully.' => 'A csoport módosítása sikeresen megtörtént', + 'Unable to update your group.' => 'Nem sikerült a csoport módosítása', + 'Add group member to "%s"' => 'Csoport tag hozzáadáasa "%s"-hez', + 'Group member added successfully.' => 'Tag hozzáadás sikeresen megtörtént', + 'Unable to add group member.' => 'Nem sikerült a tagot hozzáadni a csoporthoz.', + 'Remove user from group "%s"' => 'Felhasználó eltávolítása a "%s" csoportból', + 'User removed successfully from this group.' => 'A felhasználó sikeresen el lett távolítva a csoportból.', + 'Unable to remove this user from the group.' => 'Nem sikerült a felhasználó eltávolítása a csoportból', + 'Remove group' => 'Csoport törlése', + 'Group removed successfully.' => 'A csoport törlése sikeresen megtörtént.', + 'Unable to remove this group.' => 'Nem sikerült a csoport törlése.', + 'Project Permissions' => 'Projekt engedélyek', + 'Manager' => 'Vezető', + 'Project Manager' => 'Projekt vezető', + 'Project Member' => 'Projekt tag', + 'Project Viewer' => 'Projekt megtekintés', + 'Your account is locked for %d minutes' => 'Az ön számlája %d percre zárolva lett.', + 'Invalid captcha' => 'Érvénytelen captcha', + 'The name must be unique' => 'A névnek egyedinek kell lennie', + 'View all groups' => 'Az összes csoport megtekintése', + 'View group members' => 'A csoporttagok megtekintése', + 'There is no user available.' => 'Nincs ilyen felhasználó.', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Valóban el kívánja távolítani a "%s" felhasználót a "%s" csoportból?', + 'There is no group.' => 'Nincs ilyen csoport.', + 'External Id' => 'Külső azonosító', + 'Add group member' => 'Csoporttag hozzáadása', + 'Do you really want to remove this group: "%s"?' => 'Valóban törölni kívánja ezt a csoportot: "%s"?', + 'There is no user in this group.' => 'Nincs egy felhasználó sem ebben a csoportban.', + 'Remove this user' => 'A felhasználó eltávolítása', + 'Permissions' => 'Engedélyek', + 'Allowed Users' => 'Megengedett felhasználók', + 'No user have been allowed specifically.' => 'Egyedileg semelyik felhasználó sem lett engedélyezve.', + 'Role' => 'Szerepkör', + 'Enter user name...' => 'Adja be a felhasználó nevét...', + 'Allowed Groups' => 'Megengedett csoportok', + 'No group have been allowed specifically.' => 'Egyedileg semelyik csoport sem lett engedélyezve.', + 'Group' => 'Csoport', + 'Group Name' => 'Csoport név', + 'Enter group name...' => 'Adja meg a csoport nevét...', + 'Role:' => 'Szerepkör:', + 'Project members' => 'Projekt tagok', + 'Compare hours for "%s"' => 'Az órák összehasonlítása "%s" számára', + '%s mentioned you in the task #%d' => '%s megemlítette önt a #%d feladatban', + '%s mentioned you in a comment on the task #%d' => '%s megemlítette önt a #%d feladathoz fűzött megjegyzésben', + 'You were mentioned in the task #%d' => 'Ön meg lett említve a #%d feladatban', + 'You were mentioned in a comment on the task #%d' => 'Ön meg lett említve a #%d feladathoz fűzött megjegyzésben', + 'Mentioned' => 'Meg lett említve', + 'Compare Estimated Time vs Actual Time' => 'A becsült és a tényleges idő összehasonlítása', + 'Estimated hours: ' => 'Becsült órák: ', + 'Actual hours: ' => 'Tényleges órák: ', + 'Hours Spent' => 'Ráfordítás órákban', + 'Hours Estimated' => 'Becsült idő órákban', + 'Estimated Time' => 'Becsült idő', + 'Actual Time' => 'Tényleges idő', + 'Estimated vs actual time' => 'Becsült idő ill. tényleges idő', + 'RUB - Russian Ruble' => 'RUB - Orosz rubel', + 'Assign the task to the person who does the action when the column is changed' => 'A feladat hozzárendelése ahhoz a személyhez, aki az oszlop megváltoztatásakor intézkedik', + 'Close a task in a specific column' => 'A megadott oszlopban lévő feladat lezárása', + 'Time-based One-time Password Algorithm' => 'Idő alapú, egyszer használható jelszó algoritmus', + 'Two-Factor Provider: ' => 'Két tényezős bejelentkezés szolgáltatója', + 'Disable two-factor authentication' => 'Két tényezős bejelentkezés tiltása', + 'Enable two-factor authentication' => 'Két tényezős bejelentkezés engedélyezése', + 'There is no integration registered at the moment.' => 'Jelenleg nincs regisztrált integráció.', + 'Password Reset for Kanboard' => 'Jelszó alaphelyzetbe állítása', + 'Forgot password?' => 'Elfelejtett jelszó?', + 'Enable "Forget Password"' => '"Elfelejtett jelszó" engedélyezése', + 'Password Reset' => 'Jelszó alaphelyzetbe állíása', + 'New password' => 'Új jelszó', + 'Change Password' => 'Jelszó megváltoztatása', + 'To reset your password click on this link:' => 'A jelszó megváltoztatásáshoz kattintson erre a hivatkozásra', + 'Last Password Reset' => 'Jelszó alaphelyzetbe állítás utoljára ekkor', + 'The password has never been reinitialized.' => 'A jelszó soha sem lett alaphelyzetbe állítva.', + 'Creation' => 'Létrehozás', + 'Expiration' => 'Lejárat', + 'Password reset history' => 'A jelszó alaphelyzetbe állítás története', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'A "%s" oszlophoz és a "%s" sávhoz tartozó összes feladat sikeresen le lett zárva.', + 'Do you really want to close all tasks of this column?' => 'Valóban le kívánja zárni az oszlophoz tartozó összes feladatot?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d feladat lesz lezárva, ezek a "%s" oszlopban és a "%s" sávban vannak.', + 'Close all tasks of this column' => 'Az oszlophoz tartozó összes feladat lezárása', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Egy plugin sem regisztrált projekt értesítési módszert. Egyedi értesítések még mindig beállíthatók a felhasználói profilban.', + 'My dashboard' => 'Az én dashboard-om', + 'My profile' => 'Az én profilom', + 'Project owner: ' => 'A projekt tulajdonosa: ', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'A projekt azonosító opcionális, és kötelezően alfanumerikus karakterekből áll, pl: MYPROJECT.', + 'Project owner' => 'Projekt tulajdonos', + 'Those dates are useful for the project Gantt chart.' => 'Ezek a dátumok a projekt Gantt diagramjához hasznosak.', + 'Private projects do not have users and groups management.' => 'A privát projektekhez nem tartozik felhasználó kezelés és csoport kezelés.', + 'There is no project member.' => 'A projektnek nincs tagja.', + 'Priority' => 'Prioritás', + 'Task priority' => 'Feladat prioritás', + 'General' => 'Általános', + 'Dates' => 'Dátumok', + 'Default priority' => 'Alap prioritás', + 'Lowest priority' => 'Legalacsonyabb prioritás', + 'Highest priority' => 'Legmagasabb prioritás', + 'If you put zero to the low and high priority, this feature will be disabled.' => 'Ha nullát ad meg a legalacsonyabb és legmagasabb prioritásként, akkor ez a jellemző letiltásra kerül.', + 'Close a task when there is no activity' => 'A feladat lezárása, ha nincs tevékenység', + 'Duration in days' => 'Hossz napokban', + 'Send email when there is no activity on a task' => 'email küldése, ha egy feladathoz nincs tevékenység', + 'Unable to fetch link information.' => 'A hivatkozás adatainak sikertelen beolvasása', + 'Daily background job for tasks' => 'A feladatokhoz tartozó napi háttér munkák', + 'Auto' => 'Automatikus', + 'Related' => 'Kapcsolódó', + 'Attachment' => 'Csatolmány', + 'Title not found' => 'Nincs ilyen cím', + 'Web Link' => 'Web hivatkozás', + 'External links' => 'Külső hivatkozások', + 'Add external link' => 'Külső hivatkozás hozzáadása', + 'Type' => 'Típus', + 'Dependency' => 'Függőség', + 'Add internal link' => 'Belső hivatkozás hozzáadása', + 'Add a new external link' => 'Új külső hivatkozás hozzáadása', + 'Edit external link' => 'Külső hivatkozás szerkesztése', + 'External link' => 'Külső hivatkozás', + 'Copy and paste your link here...' => 'Másold be a hivatkozást ide...', + 'URL' => 'URL', + 'Internal links' => 'Belső hivatkozások', + 'Assign to me' => 'Rendeld hozzám', + 'Me' => 'Hozzám', + 'Do not duplicate anything' => 'Semmit se duplázz meg', + 'Projects management' => 'Projekt kezelés', + 'Users management' => 'Felhasználók kezelése', + 'Groups management' => 'Csoportok kezelése', + 'Create from another project' => 'Létrehozás egy másik projektből', + 'open' => 'nyitva', + 'closed' => 'zárva', + 'Priority:' => 'Prioritás:', + 'Reference:' => 'Hivatkozás:', + 'Complexity:' => 'Komplexitás:', + 'Swimlane:' => 'Sáv:', + 'Column:' => 'Oszlop:', + 'Position:' => 'Pozíció:', + 'Creator:' => 'Létrehozó:', + 'Time estimated:' => 'Becsült idő:', + '%s hours' => '%s óra', + 'Time spent:' => 'Eltelt idő:', + 'Created:' => 'Létrehozva:', + 'Modified:' => 'Módosítva:', + 'Completed:' => 'Elkészült:', + 'Started:' => 'Elindult:', + 'Moved:' => 'Elmozgatva:', + 'Task #%d' => '#%d. feladat', + 'Date and time format' => 'Dátum és idő formátum', + 'Time format' => 'Idő formátum', + 'Start date: ' => 'Kezdő datum: ', + 'End date: ' => 'Vég dátum: ', + 'New due date: ' => 'Új határidő: ', + 'Start date changed: ' => 'Kezdő dátum megváltozott: ', + 'Disable private projects' => 'Privát projektek tiltása', + 'Do you really want to remove this custom filter: "%s"?' => 'Valóban el kívánja távolítani ezt az egyedi szűrőt: "%s"?', + 'Remove a custom filter' => 'Egyedi szűrő eltávolítása', + 'User activated successfully.' => 'A felhasználó sikeresen aktiválva lett.', + 'Unable to enable this user.' => 'Nem sikerült a felhasználó engedélyezése.', + 'User disabled successfully.' => 'A felhaszáló sikeresen le lett tiltva.', + 'Unable to disable this user.' => 'Nem sikerült a felhasználó letiltása.', + 'All files have been uploaded successfully.' => 'Az összes fájl sikeresen feltöltődött.', + 'View uploaded files' => 'A feltöltött fájlok megtekintése', + 'The maximum allowed file size is %sB.' => 'A fájl max. megengedett mérete %s bájt', + 'Choose files again' => 'Válasszon újból fájlt', + 'Drag and drop your files here' => 'Fogdd-és-vidd módszerrel dobja ide a fájlt', + 'choose files' => 'válasszon fájlt', + 'View profile' => 'Profil megtekintés', + 'Two Factor' => 'Két tényezős', + 'Disable user' => 'Felhasználó tiltása', + 'Do you really want to disable this user: "%s"?' => 'Valóban le akarja tiltani ezt a felhasználót: "%s"?', + 'Enable user' => 'Felhasználó engedélyezése', + 'Do you really want to enable this user: "%s"?' => 'Valóban engedélyezni akarja ezt a felhasználót: "%s"?', + 'Download' => 'Letöltés', + 'Uploaded: %s' => 'Feltöltve: %s', + 'Size: %s' => 'méret %s', + 'Uploaded by %s' => 'Feltöltve %s által', + 'Filename' => 'Fájlnév', + 'Size' => 'Méret', + 'Column created successfully.' => 'Az oszlop sikeresen létrejött', + 'Another column with the same name exists in the project' => 'A projektben már létezik egy ugyanilyen nevű oszlop', + 'Default filters' => 'Alap szűrők', + 'Your board doesn\'t have any columns!' => 'A táblának nincsenek oszlopai!', + 'Change column position' => 'Oszlop helyzetének megváltoztatása', + 'Switch to the project overview' => 'Projektek áttekintése', + 'User filters' => 'Felhasználók szűrése', + 'Category filters' => 'Kategóriák szűrése', + 'Upload a file' => 'Fájl feltöltése', + 'View file' => 'Fájl megtekintése', + 'Last activity' => 'Utolsó aktivitás', + 'Change subtask position' => 'Részfeladat helyzetének megváltoztatása', + 'This value must be greater than %d' => 'Ennek az értéknek nagyobbnak kell lennie, mint %d', + 'Another swimlane with the same name exists in the project' => 'A projektben létezik egy ugyanilyen nevű másik sáv', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Példa: http://example.kanboard.net/ (ez abszolút URL-ek előállítására használható)', + 'Actions duplicated successfully.' => 'A tevékenység sikeresen meg lett duplázva.', + 'Unable to duplicate actions.' => 'A tevékenység megduplázása nem sikerült.', + 'Add a new action' => 'Új tevékenység létrehozása', + 'Import from another project' => 'Importálás egy másik projektből', + 'There is no action at the moment.' => 'Jelenleg nincs egy tevkenység sem.', + 'Import actions from another project' => 'Tevékenységek importálása egy másik projektből', + 'There is no available project.' => 'Nincs rendelkezésre álló projekt.', + 'Local File' => 'Helyi fájl', + 'Configuration' => 'Konfiguráció', + 'PHP version:' => 'PHP verzió:', + 'PHP SAPI:' => 'PHP SAPI:', + 'OS version:' => 'Operációs rendszer verzió:', + 'Database version:' => 'Adatbázis verzió:', + 'Browser:' => 'Böngésző:', + 'Task view' => 'Feladat nézet', + 'Edit task' => 'Feladat szerkesztése', + 'Edit description' => 'Leírás szerkesztése', + 'New internal link' => 'Új belső hivatkozás', + 'Display list of keyboard shortcuts' => 'Billentyű parancsok megjelenítése', + 'Menu' => 'Menü', + 'Set start date' => 'Kezdő időpont beállítása', + 'Avatar' => 'Avatár', + 'Upload my avatar image' => 'Kép feltöltése', + 'Remove my image' => 'Kép törése', + 'The OAuth2 state parameter is invalid' => 'Az OAuth2 állapot paraméter érvénytelen', + 'User not found.' => 'Nincs ilyen felhasználó.', + 'Search in activity stream' => 'Keresés a tevékenységek között', + 'My activities' => 'Tevékenységeim', + 'Activity until yesterday' => 'Tevékenységek tegnapig', + 'Activity until today' => 'Tevékenységek a mai napig', + 'Search by creator: ' => 'Keresés a létrehozó szerint: ', + 'Search by creation date: ' => 'Keresés a létrehozás ideje szerint: ', + 'Search by task status: ' => 'Keresés a feladat állapota szerint: ', + 'Search by task title: ' => 'Keresés a feladat címe szerint: ', + 'Activity stream search' => 'Tevékenységi láncban történő keresés', + 'Projects where "%s" is manager' => 'Azok a projektek, amelyekben "%s" vezető', + 'Projects where "%s" is member' => 'Azok a projektek, amelyekben "%s" tag', + 'Open tasks assigned to "%s"' => '"%s" -hez rendelt nyitott feladatok', + 'Closed tasks assigned to "%s"' => '"%s" -hez rendelt lezárt feladatok', + 'Assign automatically a color based on a priority' => 'A prioritás alapján szín automatikus hozzárendelése', + 'Overdue tasks for the project(s) "%s"' => 'A "%s" projekt(ek)hez tartozó, határidőt túllépő feladatok', + 'Upload files' => 'Fájlok feltöltése', + 'Installed Plugins' => 'Installált plugin-ek', + 'Plugin Directory' => 'Plugin könyvtár', + 'Plugin installed successfully.' => 'A plugin installálása sikerült.', + 'Plugin updated successfully.' => 'A plugin módosítása sikerült.', + 'Plugin removed successfully.' => 'A plugin törlése sikerült.', + 'Subtask converted to task successfully.' => 'Az alfeladat feladattá történő átalakítása sikerült.', + 'Unable to convert the subtask.' => 'Az alfeladat átalakítása nem sikerült.', + 'Unable to extract plugin archive.' => 'A plugin archívum kibontása nem sikerült.', + 'Plugin not found.' => 'A plugin nem található.', + 'You don\'t have the permission to remove this plugin.' => 'Nincs joga ennek a plugin-nek az eltávolításához.', + 'Unable to download plugin archive.' => 'A plugin archívum letöltése nem sikerült.', + 'Unable to write temporary file for plugin.' => 'A plugin-hez nem sikerült egy átmeneti fájl létrehozása.', + 'Unable to open plugin archive.' => 'Nem sikerült a plugin archívum megnyitása.', + 'There is no file in the plugin archive.' => 'A plugin archívumban nincs egyetelen egy fájl sem.', + 'Create tasks in bulk' => 'Feladatok tömeges létrehozása', + 'Your Kanboard instance is not configured to install plugins from the user interface.' => 'Az Ön Kanboard példánya úgy lett konfigurálva, hogy ne lehessen plugin-eket installálni a felhasználói felületről.', + 'There is no plugin available.' => 'Nem áll plugin rendelkezésre.', + 'Install' => 'Installálás', + 'Update' => 'Módosítás', + 'Up to date' => 'Naprakész', + 'Not available' => 'Nem áll rendelkezésre', + 'Remove plugin' => 'Plugin eltávolítása', + 'Do you really want to remove this plugin: "%s"?' => 'Valóban el kívánja távolítani ezt a plugin-t: "%s"?', + // 'Uninstall' => '', + 'Listing' => 'Listázás', + 'Metadata' => 'Metaadat', + 'Manage projects' => 'Projektek kezelése', + 'Convert to task' => 'Feladattá történő átalakítás', + 'Convert sub-task to task' => 'Alfeladat feladattá történő átalakítása', + 'Do you really want to convert this sub-task to a task?' => 'Valóban feladattá akarja átalakítani ezt az alfeladatot?', + 'My task title' => 'A feladat címe', + 'Enter one task by line.' => 'Minden sorban egy feladatot adjon meg.', + 'Number of failed login:' => 'A sikertelen bejelentkezések száma:', + 'Account locked until:' => 'A számla zárolva a következő időpontig:', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/id_ID/translations.php b/sources/app/Locale/id_ID/translations.php index 3f10505..d377651 100644 --- a/sources/app/Locale/id_ID/translations.php +++ b/sources/app/Locale/id_ID/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Lihat tugas ini', 'Remove user' => 'Hapus pengguna', 'Do you really want to remove this user: "%s"?' => 'Anda yakin akan menghapus pengguna ini : « %s » ?', - 'New user' => 'Pengguna baru', 'All users' => 'Semua pengguna', 'Username' => 'Nama pengguna', 'Password' => 'Kata sandi', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d tugas yang ditutup', 'No task for this project' => 'Tidak ada tugas dalam proyek ini', 'Public link' => 'Tautan publik', - 'Change assignee' => 'Mengubah orang yand ditugaskan', - 'Change assignee for the task "%s"' => 'Mengubah orang yang ditugaskan untuk tugas « %s »', 'Timezone' => 'Zona waktu', 'Sorry, I didn\'t find this information in my database!' => 'Maaf, saya tidak menemukan informasi ini dalam basis data saya !', 'Page not found' => 'Halaman tidak ditemukan', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Tinggalkan deskripsi', 'Comment added successfully.' => 'Komentar berhasil ditambahkan.', 'Unable to create your comment.' => 'Tidak dapat menambahkan komentar anda.', - 'Edit this task' => 'Modifikasi tugas ini', 'Due Date' => 'Batas Tanggal Terakhir', 'Invalid date' => 'Tanggal tidak valid', 'Automatic actions' => 'Tindakan otomatis', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategori', 'Category:' => 'Kategori :', 'Categories' => 'Kategori', - 'Category not found.' => 'Kategori tidak ditemukan', 'Your category have been created successfully.' => 'Kategori anda berhasil dibuat.', 'Unable to create your category.' => 'Tidak dapat membuat kategori anda.', 'Your category have been updated successfully.' => 'Kategori anda berhasil diperbaharui.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Apakah anda yakin akan menghapus berkas ini « %s » ?', 'Attachments' => 'Lampiran', 'Edit the task' => 'Modifikasi tugas', - 'Edit the description' => 'Modifikasi deskripsi', 'Add a comment' => 'Tambahkan komentar', 'Edit a comment' => 'Modifikasi komentar', 'Summary' => 'Ringkasan', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Tidak ada otentifikasi eksternal yang aktif.', 'Password modified successfully.' => 'Kata sandi berhasil dimodifikasi.', 'Unable to change the password.' => 'Tidak dapat merubah kata sandir.', - 'Change category for the task "%s"' => 'Rubah kategori untuk tugas « %s »', 'Change category' => 'Rubah kategori', '%s updated the task %s' => '%s memperbaharui tugas %s', '%s opened the task %s' => '%s membuka tugas %s', @@ -413,9 +406,7 @@ return array( 'About' => 'Tentang', 'Database driver:' => 'Driver basis data :', 'Board settings' => 'Pengaturan papan', - 'URL and token' => 'URL dan token', 'Webhook settings' => 'Pengaturan webhook', - 'URL for task creation:' => 'URL untuk pembuatan tugas :', 'Reset token' => 'Mereset token', 'API endpoint:' => 'API endpoint :', 'Refresh interval for private board' => 'Interval pembaruan untuk papan pribadi', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Supprimer une swimlane', 'Show default swimlane' => 'Perlihatkan standar swimlane', 'Swimlane modification for the project "%s"' => 'Modifikasi swimlane untuk proyek « %s »', - 'Swimlane not found.' => 'Swimlane tidak ditemukan.', 'Swimlane removed successfully.' => 'Swimlane berhasil dihapus.', 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane berhasil diperbaharui.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Semua swimlane', 'All colors' => 'Semua warna', 'Moved to column %s' => 'Pindah ke kolom %s', - 'Change description' => 'Rubah deskripsi', 'User dashboard' => 'Dasbor pengguna', 'Allow only one subtask in progress at the same time for a user' => 'Izinkan hanya satu subtugas dalam proses secara bersamaan untuk satu pengguna', 'Edit column "%s"' => 'Modifikasi kolom « %s »', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'lihat papan di Kanboard', 'The task have been moved to the first swimlane' => 'Tugas telah dipindahkan ke swimlane pertama', 'The task have been moved to another swimlane:' => 'Tugas telah dipindahkan ke swimlane lain:', - 'Overdue tasks for the project "%s"' => 'Tugas terlambat untuk proyek « %s »', 'New title: %s' => 'Judul baru : %s', 'The task is not assigned anymore' => 'Tugas tidak ditugaskan lagi', 'New assignee: %s' => 'Penerima baru : %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Hentikan timer', 'Start timer' => 'Mulai timer', 'Add project member' => 'Tambahkan anggota proyek', - 'Enable notifications' => 'Aktifkan pemberitahuan', 'My activity stream' => 'Aliran kegiatan saya', 'My calendar' => 'Kalender saya', 'Search tasks' => 'Cari tugas', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/it_IT/translations.php b/sources/app/Locale/it_IT/translations.php index 93ceb03..4ae1419 100644 --- a/sources/app/Locale/it_IT/translations.php +++ b/sources/app/Locale/it_IT/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Visualizza questo task', 'Remove user' => 'Cancella un utente', 'Do you really want to remove this user: "%s"?' => 'Veramente vuoi cancellare questo utente: "%s" ?', - 'New user' => 'Aggiungi un utente', 'All users' => 'Tutti gli utenti', 'Username' => 'Nome utente', 'Password' => 'Password', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d task chiusi', 'No task for this project' => 'Nessun task per questo progetto', 'Public link' => 'Link pubblico', - 'Change assignee' => 'Cambia l\'assegnatario', - 'Change assignee for the task "%s"' => 'Cambia l\'assegnatario per il task "%s"', 'Timezone' => 'Fuso orario', 'Sorry, I didn\'t find this information in my database!' => 'Spiacente, non ho trovato questa informazione sul database!', 'Page not found' => 'Pagina non trovata', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Lascia una descrizione', 'Comment added successfully.' => 'Commenti aggiunti con successo.', 'Unable to create your comment.' => 'Impossibile creare questo commento.', - 'Edit this task' => 'Modifica questo task', 'Due Date' => 'Data di scadenza', 'Invalid date' => 'Data non valida', 'Automatic actions' => 'Azioni automatiche', @@ -250,7 +246,6 @@ return array( 'Category' => 'Categoria', 'Category:' => 'Categoria:', 'Categories' => 'Categorie', - 'Category not found.' => 'Categoria non trovata.', 'Your category have been created successfully.' => 'La tua categoria è stata creata con successo.', 'Unable to create your category.' => 'Impossibile creare la tua categoria.', 'Your category have been updated successfully.' => 'La tua categoria è stata aggiornata con successo.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Vuoi davvero cancellare questo file: "%s"?', 'Attachments' => 'Allegati', 'Edit the task' => 'Modifica il task', - 'Edit the description' => 'Modifica la descrizione', 'Add a comment' => 'Aggiungi un commento', 'Edit a comment' => 'Modifica un commento', 'Summary' => 'Sommario', @@ -370,7 +364,6 @@ return array( '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 task "%s"', 'Change category' => 'Cambia categoria', '%s updated the task %s' => '%s ha aggiornato il task %s', '%s opened the task %s' => '%s ha aperto il task %s', @@ -413,9 +406,7 @@ return array( 'About' => 'Informazioni', 'Database driver:' => 'Driver per Database', 'Board settings' => 'Impostazioni bacheca', - 'URL and token' => 'URL e token', 'Webhook settings' => 'Impostazione Webhook', - 'URL for task creation:' => 'URL per la creazione dei task:', 'Reset token' => 'Rigenera il token', 'API endpoint:' => 'Endpoint dell\'API:', 'Refresh interval for private board' => 'Intervallo di refresh per le bacheche private', @@ -435,7 +426,7 @@ return array( 'Time estimated' => 'Tempo stimato', 'There is nothing assigned to you.' => 'Non c\'è nulla assegnato a te.', 'My tasks' => 'I miei task', - 'Activity stream' => 'Flusso di attività', + 'Activity stream' => 'Flusso attività', 'Dashboard' => 'Bacheca', 'Confirmation' => 'Conferma', 'Allow everybody to access to this project' => 'Abilita tutti ad accedere a questo progetto', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Rimuovi una corsia', 'Show default swimlane' => 'Mostra la corsia predefinita', 'Swimlane modification for the project "%s"' => 'Modifica corsia per il progetto "%s"', - 'Swimlane not found.' => 'Corsia non trovata.', 'Swimlane removed successfully.' => 'Corsia rimossa con successo.', 'Swimlanes' => 'Corsie', 'Swimlane updated successfully.' => 'Corsia aggiornata con successo.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Tutte le corsie', 'All colors' => 'Tutti i colori', '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-task in corso per utente alla volta', 'Edit column "%s"' => 'Modifica la colonna "%s"', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'guarda la bacheca su Kanboard', 'The task have been moved to the first swimlane' => 'Il task è stato spostato nella prima corsia', 'The task have been moved to another swimlane:' => 'Il task è stato spostato in un\'altra corsia:', - 'Overdue tasks for the project "%s"' => 'Task scaduti per il progetto "%s"', 'New title: %s' => 'Nuovo titolo: %s', 'The task is not assigned anymore' => 'Il task non è più assegnato a nessuno', 'New assignee: %s' => 'Nuovo assegnatario: %s', @@ -738,8 +726,7 @@ return array( 'Stop timer' => 'Ferma il timer', 'Start timer' => 'Avvia il timer', 'Add project member' => 'Aggiungi un membro di progetto', - 'Enable notifications' => 'Abilita notifiche', - 'My activity stream' => 'Il mio flusso di attività', + 'My activity stream' => 'Il mio flusso attività', 'My calendar' => 'Il mio calendario', 'Search tasks' => 'Ricerca task', 'Reset filters' => 'Annulla filtri', @@ -762,7 +749,7 @@ return array( 'Keyboard shortcut: "%s"' => 'Scorciatoia da tastiera: "%s"', 'List' => 'Lista', 'Filter' => 'Filtro', - 'Advanced search' => 'Riceca avanzata', + 'Advanced search' => 'Ricerca avanzata', 'Example of query: ' => 'Esempio di query: ', 'Search by project: ' => 'Ricerca per progetto: ', 'Search by column: ' => 'Ricerca per colonna: ', @@ -833,7 +820,7 @@ return array( 'Users overview' => 'Panoramica utenti', 'Members' => 'Membri', 'Shared project' => 'Progetto condiviso', - 'Project managers' => 'Manager del progetto', + 'Project managers' => 'Manager di progetto', 'Gantt chart for all projects' => 'Grafico Gantt per tutti i progetti', 'Projects list' => 'Elenco progetti', 'Gantt chart for this project' => 'Grafico Gantt per questo progetto', @@ -980,7 +967,7 @@ return array( 'Group Name' => 'Nome del gruppo', 'Enter group name...' => 'Inserisci il nome del gruppo...', 'Role:' => 'Ruolo:', - 'Project members' => 'Membri del progetto', + 'Project members' => 'Membri di progetto', 'Compare hours for "%s"' => 'Confronta le ore per "%s"', '%s mentioned you in the task #%d' => '%s ti ha menzionato nel task #%d', '%s mentioned you in a comment on the task #%d' => '%s ti ha menzionato in un commento del task #%d', @@ -1058,7 +1045,7 @@ return array( // 'URL' => '', 'Internal links' => 'Link interni', 'Assign to me' => 'Assegna a me', - // 'Me' => '', + 'Me' => 'Me stesso', 'Do not duplicate anything' => 'Non duplicare nulla', 'Projects management' => 'Gestione progetti', 'Users management' => 'Gestione utenti', @@ -1116,7 +1103,7 @@ return array( 'Column created successfully.' => 'Colonna creata con successo.', 'Another column with the same name exists in the project' => 'Un\'altra colonna con lo stesso nome è già esistente in questo progetto', 'Default filters' => 'Filtri predefiniti', - 'Your board doesn\'t have any column!' => 'La tua bacheca non ha nessuna colonna!', + 'Your board doesn\'t have any columns!' => 'La tua bacheca non ha nessuna colonna!', 'Change column position' => 'Modifica la posizione della colonna', 'Switch to the project overview' => 'Passa alla panoramica di progetto', 'User filters' => 'Filtri utente', @@ -1128,29 +1115,105 @@ return array( 'This value must be greater than %d' => 'Questo valore deve essere magiore di %d', 'Another swimlane with the same name exists in the project' => 'Un\'altra corsia con lo stesso nome è già esistente in questo progetto', 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Esempio: http://example.kanboard.net/ (usato per generare URL assolute)', - // 'Actions duplicated successfully.' => '', - // 'Unable to duplicate actions.' => '', - // 'Add a new action' => '', - // 'Import from another project' => '', - // 'There is no action at the moment.' => '', - // 'Import actions from another project' => '', - // 'There is no available project.' => '', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', + 'Actions duplicated successfully.' => 'Azioni duplicate con successo.', + 'Unable to duplicate actions.' => 'Impossibile duplicare le azioni.', + 'Add a new action' => 'Aggiungi una nuova azione', + 'Import from another project' => 'Importa da un altro progetto', + 'There is no action at the moment.' => 'Nessuna azione disponibile al momento.', + 'Import actions from another project' => 'Importa azioni da un altro progetto', + 'There is no available project.' => 'Nessun progetto disponibile.', + 'Local File' => 'File locale', + 'Configuration' => 'Configurazione', + 'PHP version:' => 'Versione PHP:', // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', + 'OS version:' => 'Versione OS:', + 'Database version:' => 'Versione database:', // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', + 'Task view' => 'Vista dei task', + 'Edit task' => 'Modifica task', + 'Edit description' => 'Modifica descrizione', + 'New internal link' => 'Nuovo link interno', + 'Display list of keyboard shortcuts' => 'Mostra una lista di scorciatoie da tastiera', // 'Menu' => '', - // 'Set start date' => '', + 'Set start date' => 'Imposta la data di inzio', // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'Upload my avatar image' => 'Carica l\'immagine del mio avatar', + 'Remove my image' => 'Rimuovi la mia immagine', + 'The OAuth2 state parameter is invalid' => 'Il parametro di stato OAuth2 non è valido.', + 'User not found.' => 'Utente non trovato.', + 'Search in activity stream' => 'Ricerca nel mio flusso attività', + 'My activities' => 'Le mie attività', + 'Activity until yesterday' => 'Attività ad oggi', + 'Activity until today' => 'Attività fino a ieri', + 'Search by creator: ' => 'Ricerca per creatore: ', + 'Search by creation date: ' => 'Ricerca per data di creazione: ', + 'Search by task status: ' => 'Ricerca per stato del task: ', + 'Search by task title: ' => 'Ricerca per titolo del task: ', + 'Activity stream search' => 'Ricerca nel flusso attività', + 'Projects where "%s" is manager' => 'Progetti all\'interno dei quali "%s" è manager', + 'Projects where "%s" is member' => 'Progetti all\'interno dei quali "%s" è membro', + 'Open tasks assigned to "%s"' => 'Task aperti assegnati a "%s"', + 'Closed tasks assigned to "%s"' => 'Task chiusi assegnati a "%s"', + 'Assign automatically a color based on a priority' => 'Assegna automaticamente un colore in base alla priorità', + 'Overdue tasks for the project(s) "%s"' => 'Task scaduti per il progetto "%s"', + 'Upload files' => 'Carica file', + 'Installed Plugins' => 'Plugin installati', + 'Plugin Directory' => 'Directory dei plugin', + 'Plugin installed successfully.' => 'Plugin installato con successo.', + 'Plugin updated successfully.' => 'Plugin aggiornato con successo.', + 'Plugin removed successfully.' => 'Plugin rimosso con successo.', + 'Subtask converted to task successfully.' => 'Sotto-task converito in task con successo.', + 'Unable to convert the subtask.' => 'Impossibile convertire il sotto-task.', + 'Unable to extract plugin archive.' => 'Impossibile estrarre l\' archivio del plugin.', + 'Plugin not found.' => 'Plugin non trovato.', + 'You don\'t have the permission to remove this plugin.' => 'Non hai i permessi per rimuovere questo plugin.', + 'Unable to download plugin archive.' => 'Impossibile scaricare l\'archivo del plugin', + 'Unable to write temporary file for plugin.' => 'Impossibile scrivere il file temporaneo per il plugin.', + 'Unable to open plugin archive.' => 'Impossibile aprire l\'archivio del plugin.', + 'There is no file in the plugin archive.' => 'Non ci sono file nell\' archivio del plugin.', + 'Create tasks in bulk' => 'Creazione massiva di task', + 'Your Kanboard instance is not configured to install plugins from the user interface.' => 'La tua installazione Kanboard non è configurata per installare plugin tramite l\'interfaccia utente.', + 'There is no plugin available.' => 'Non ci sono plugin disponibili.', + 'Install' => 'Installa', + 'Update' => 'Aggiorna', + 'Up to date' => 'Aggiornato', + 'Not available' => 'Non disponibile', + 'Remove plugin' => 'Disinstalla plugin', + 'Do you really want to remove this plugin: "%s"?' => 'Vuoi davvero rimuovere questo plugin: "%s"?', + 'Uninstall' => 'Disinstalla', + 'Listing' => 'Elenco', + 'Metadata' => 'Metadati', + 'Manage projects' => 'Gestione progetti', + 'Convert to task' => 'Converti in task', + 'Convert sub-task to task' => 'Converti il sotto-task in task', + 'Do you really want to convert this sub-task to a task?' => 'Vuoi davvero convertire questo sotto-task in un task?', + 'My task title' => 'Titolo del mio task', + 'Enter one task by line.' => 'Inserisci un task per ogni riga.', + 'Number of failed login:' => 'Numero di login falliti:', + 'Account locked until:' => 'Account bloccato fino al:', + 'Email settings' => 'Impostazioni Email', + 'Email sender address' => 'Indirizzo Email mittente', + 'Email transport' => 'Trasporto Email', + // 'Webhook token' => '', + 'Imports' => 'Importa', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/ja_JP/translations.php b/sources/app/Locale/ja_JP/translations.php index b48eabd..692afb7 100644 --- a/sources/app/Locale/ja_JP/translations.php +++ b/sources/app/Locale/ja_JP/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'このタクスを見る', 'Remove user' => 'ユーザの削除', 'Do you really want to remove this user: "%s"?' => 'ユーザ「%s」を本当に削除しますか?', - 'New user' => 'ユーザを追加する', 'All users' => 'すべてのユーザ', 'Username' => 'ユーザ名', 'Password' => 'パスワード', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d 個のクローズしたタスク', 'No task for this project' => 'このプロジェクトにタスクがありません', 'Public link' => '公開アクセス用リンク', - 'Change assignee' => '担当を変更する', - 'Change assignee for the task "%s"' => 'タスク「%s」の担当を変更する', 'Timezone' => 'タイムゾーン', 'Sorry, I didn\'t find this information in my database!' => 'データベース上で情報が見つかりませんでした!', 'Page not found' => 'ページが見つかりません', @@ -170,7 +167,6 @@ return array( 'Leave a description' => '説明を書く', 'Comment added successfully.' => 'コメントを追加しました。', 'Unable to create your comment.' => 'コメントの追加に失敗しました。', - 'Edit this task' => 'タスクを変更する', 'Due Date' => '期限', 'Invalid date' => '日付が無効です', 'Automatic actions' => '自動アクションを管理する', @@ -250,7 +246,6 @@ return array( 'Category' => 'カテゴリ', 'Category:' => 'カテゴリ:', 'Categories' => 'カテゴリ', - 'Category not found.' => 'カテゴリが見つかりません', 'Your category have been created successfully.' => 'カテゴリを作成しました。', 'Unable to create your category.' => 'カテゴリの作成に失敗しました。', 'Your category have been updated successfully.' => 'カテゴリを更新しました。', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'ファイル「%s」を削除しますか?', 'Attachments' => '添付', 'Edit the task' => 'タスクを変更する', - 'Edit the description' => '説明を変更する', 'Add a comment' => 'コメントの追加', 'Edit a comment' => 'コメントを変更する', 'Summary' => '概要', @@ -370,7 +364,6 @@ return array( '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 をオープンしました', @@ -413,9 +406,7 @@ return array( 'About' => '情報', 'Database driver:' => 'データベースドライバ:', 'Board settings' => '基本設定', - 'URL and token' => 'URL とトークン', 'Webhook settings' => 'Webhook の設定', - 'URL for task creation:' => 'Task 作成の URL:', 'Reset token' => 'トークンのリセット', 'API endpoint:' => 'API エンドポイント:', 'Refresh interval for private board' => '非公開ボードの更新頻度', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'スイムレーンの削除', 'Show default swimlane' => 'デフォルトスイムレーンの表示', 'Swimlane modification for the project "%s"' => '「%s」に対するスイムレーン変更', - 'Swimlane not found.' => 'スイムレーンが見つかりません。', 'Swimlane removed successfully.' => 'スイムレーンを削除しました。', 'Swimlanes' => 'スイムレーン', 'Swimlane updated successfully.' => 'スイムレーンを更新しました。', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => '全てのスイムレーン', 'All colors' => '全ての色', 'Moved to column %s' => 'カラム %s へ移動しました', - 'Change description' => '説明を変更', 'User dashboard' => 'ユーザダッシュボード', 'Allow only one subtask in progress at the same time for a user' => '一人のユーザにつき一つのタスクのみ進行中にできます', 'Edit column "%s"' => 'カラム「%s」の編集', @@ -709,7 +698,6 @@ return array( // 'view the board on Kanboard' => '', // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', // 'New title: %s' => '', // 'The task is not assigned anymore' => '', // 'New assignee: %s' => '', @@ -738,7 +726,6 @@ return array( // 'Stop timer' => '', // 'Start timer' => '', // 'Add project member' => '', - // 'Enable notifications' => '', // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/ko_KR/translations.php b/sources/app/Locale/ko_KR/translations.php index 8379761..75d6be4 100644 --- a/sources/app/Locale/ko_KR/translations.php +++ b/sources/app/Locale/ko_KR/translations.php @@ -20,14 +20,14 @@ return array( 'Orange' => '주황', 'Grey' => '회색', 'Brown' => '브라운', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Deep Orange' => '진홍', + 'Dark Grey' => '암회', + 'Pink' => '핑크', + 'Teal' => '암녹', + 'Cyan' => '청녹', + 'Lime' => '라임', + 'Light Green' => '연회', + 'Amber' => '호박', 'Save' => '저장', 'Login' => '로그인', 'Official website:' => '공식 웹사이트:', @@ -35,7 +35,6 @@ return array( 'View this task' => '이 할일 보기', 'Remove user' => '사용자 삭제', 'Do you really want to remove this user: "%s"?' => '사용자 "%s"를 정말로 삭제하시겠습니까?', - 'New user' => '사용자를 추가하는 ', 'All users' => '모든 사용자', 'Username' => '사용자 이름', 'Password' => '패스워드', @@ -66,9 +65,9 @@ return array( 'Disable' => '비활성화', 'Enable' => '유효하게 한다', 'New project' => '새 프로젝트', - // 'Do you really want to remove this project: "%s"?' => '', + 'Do you really want to remove this project: "%s"?' => '프로젝트를 삭제하시겠습니까: "%s"?', 'Remove project' => '프로젝트의 삭제', - // 'Edit the board for "%s"' => '', + 'Edit the board for "%s"' => '"%s"를 위한 보드 수정', 'All projects' => '모든 프로젝트', 'Add a new column' => '칼럼의 추가', 'Title' => '제목', @@ -76,7 +75,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"?' => '', + 'Do you really want to remove this column: "%s"?' => '칼럼을 삭제하시겠습니까: "%s"?', 'This action will REMOVE ALL TASKS associated to this column!' => '이 조작은 이 컬럼에 할당된 『 모든 할일을 삭제 』합니다!', 'Settings' => '설정', 'Application settings' => '애플리케이션의 설정', @@ -96,17 +95,17 @@ return array( 'Create another task' => '다른 할일 추가', 'New task' => '새로운 할일', 'Open a task' => '할일 열기', - // 'Do you really want to open this task: "%s"?' => '', + 'Do you really want to open this task: "%s"?' => '할일은 시작 하시겠습니까: "%s"?', 'Back to the board' => '보드로 돌아가기', 'There is nobody assigned' => '담당자가 없습니다', 'Column on the board:' => '칼럼:', 'Close this task' => '할일 마치기', - 'Open this task' => '할일을 열다', - 'There is no description.' => '설명이 없다', - 'Add a new task' => '할일을 추가하는 ', + 'Open this task' => '할일 열기', + 'There is no description.' => '설명이 없습니다', + 'Add a new task' => '할일 추가 ', 'The username is required' => '사용자 이름이 필요합니다', - // 'The maximum length is %d characters' => '', - // 'The minimum length is %d characters' => '', + 'The 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' => '사용자 이름이 이미 사용되고 있습니다', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d개의 마친 할일', 'No task for this project' => '이 프로젝트에 할일이 없습니다', 'Public link' => '공개 접속 링크', - 'Change assignee' => '담당자 변경', - 'Change assignee for the task "%s"' => '할일 "%s"의 담당자를 변경', 'Timezone' => '시간대', 'Sorry, I didn\'t find this information in my database!' => '데이터베이스에서 정보가 발견되지 않았습니다!', 'Page not found' => '페이지가 발견되지 않는다', @@ -170,7 +167,6 @@ return array( 'Leave a description' => '설명을 입력하세요', 'Comment added successfully.' => '의견을 추가했습니다.', 'Unable to create your comment.' => '댓글의 추가에 실패했습니다.', - 'Edit this task' => '할일 수정', 'Due Date' => '마감일', 'Invalid date' => '날짜가 무효입니다', 'Automatic actions' => '자동액션 관리', @@ -179,7 +175,7 @@ return array( 'Remove an action' => '자동 액션의 삭제', 'Unable to remove this action.' => '자동 액션의 삭제에 실패했습니다.', 'Action removed successfully.' => '자동 액션의 삭제에 성공했어요.', - // 'Automatic actions for the project "%s"' => '', + 'Automatic actions for the project "%s"' => '"%s" 프로젝트를 위한 자동 액션', 'Add an action' => '자동 액션 추가', 'Event name' => '이벤트 이름', 'Action name' => '액션 이름', @@ -189,19 +185,19 @@ return array( 'When the selected event occurs execute the corresponding action.' => '선택된 이벤트가 발생했을 때 대응하는 액션을 실행한다.', 'Next step' => '다음 단계', 'Define action parameters' => '액션의 바로미터', - // 'Do you really want to remove this action: "%s"?' => '', + 'Do you really want to remove this action: "%s"?' => '액션을 삭제하시겠습니까: "%s"?', 'Remove an automatic action' => '자동 액션의 삭제', 'Assign the task to a specific user' => '할일 담당자를 할당', 'Assign the task to the person who does the action' => '액션을 일으킨 사용자를 담당자이자', - 'Duplicate the task to another project' => ' 다른 프로젝트에 할일을 복제하는 ', - 'Move a task to another column' => '할일을 다른 칼럼에 이동하는 ', + 'Duplicate the task to another project' => '할일을 다른 프로젝트로 복사', + 'Move a task to another column' => '할일을 다른 칼럼으로 이동', 'Task modification' => '할일 변경', - 'Task creation' => '할일을 만들', - 'Closing a task' => '할일을 닫혔다', - 'Assign a color to a specific user' => '색을 사용자에 할당', + 'Task creation' => '할일 만들기', + 'Closing a task' => '할일 종료', + 'Assign a color to a specific user' => '사용자 색 할당', 'Column title' => '칼럼의 제목', 'Position' => '위치', - 'Duplicate to another project' => '다른 프로젝트에 복사', + 'Duplicate to another project' => '다른 프로젝트로 복사', 'Duplicate' => '복사', 'link' => '링크', 'Comment updated successfully.' => '댓글을 갱신했습니다.', @@ -235,22 +231,21 @@ return array( '%d comments' => '%d개의 댓글', '%d comment' => '%d개의 댓글', 'Email address invalid' => '메일 주소가 올바르지 않습니다.', - // 'Your external account is not linked anymore to your profile.' => '', - // 'Unable to unlink your external account.' => '', - // 'External authentication failed' => '', - // 'Your external account is linked to your profile successfully.' => '', + 'Your external account is not linked anymore to your profile.' => '외부 계정과 프로필이 더이상 연결되지 않습니다', + 'Unable to unlink your external account.' => '외부 계정과 연결 해제에 실패하였습니다', + 'External authentication failed' => '외부 인증 실패', + 'Your external account is linked to your profile successfully.' => '외부 계정과 프로필이 성공적으로 연결되었습니다', 'Email' => '이메일', 'Task removed successfully.' => '할일을 삭제했습니다.', - 'Unable to remove this task.' => '할일 삭제에 실패했습니다.', + 'Unable to remove this task.' => '할일 삭제에 실패하였습니다', 'Remove a task' => '할일 삭제', - // 'Do you really want to remove this task: "%s"?' => '', + '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.' => '카테고리를 갱신했습니다.', @@ -258,10 +253,10 @@ return array( 'Remove a category' => '카테고리의 삭제', 'Category removed successfully.' => '카테고리를 삭제했습니다.', 'Unable to remove this category.' => '카테고리를 삭제할 수 없었습니다.', - // 'Category modification for the project "%s"' => '', + 'Category modification for the project "%s"' => '"%s" 프로젝트 카테고리 수정', 'Category Name' => '카테고리 이름', 'Add a new category' => '카테고리의 추가', - // 'Do you really want to remove this category: "%s"?' => '', + 'Do you really want to remove this category: "%s"?' => '카테고리를 삭제하시겠습니까: "%s"?', 'All categories' => '모든 카테고리', 'No category' => '카테고리 없음', 'The name is required' => '이름을 입력하십시오', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => '파일 "%s" 을 삭제할까요?', 'Attachments' => '첨부', 'Edit the task' => '할일 수정', - 'Edit the description' => '설명 수정', 'Add a comment' => '댓글 추가', 'Edit a comment' => '댓글 수정', 'Summary' => '개요', @@ -305,7 +299,7 @@ return array( 'Display another project' => '프로젝트 보기', 'Created by %s' => '작성자 %s', 'Tasks Export' => '할일 내보내기', - // 'Tasks exportation for "%s"' => '', + 'Tasks exportation for "%s"' => '"%s" 할일 내보내기', 'Start Date' => '시작일', 'End Date' => '종료일', 'Execute' => '실행', @@ -318,15 +312,15 @@ return array( 'Unable to clone this project.' => '프로젝트의 복제에 실패했습니다.', 'Enable email notifications' => '이메일 알림 설정', 'Task position:' => '할일 위치:', - // 'The task #%d have been opened.' => '', - // 'The task #%d have been closed.' => '', + '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"' => '', + 'New attachment added "%s"' => '"%s"의 새로운 첨부 파일', 'New comment posted by %s' => '"%s"님이 댓글을 추가하였습니다', 'New attachment' => ' 새로운 첨부 파일', 'New comment' => ' 새로운 댓글', @@ -343,8 +337,8 @@ return array( 'Disable public access' => '공개 접속 비활성화', 'Enable public access' => '공개 접속 활성화', 'Public access disabled' => '공개 접속 불가', - // 'Do you really want to disable this project: "%s"?' => '', - // 'Do you really want to enable this project: "%s"?' => '', + 'Do you really want to disable this project: "%s"?' => '프로젝트를 비활성화하시겠습니까: "%s"?', + 'Do you really want to enable this project: "%s"?' => '프로젝트를 활성화하시겠습니까: "%s"?', 'Project activation' => '프로젝트의 액티베ー션', 'Move the task to another project' => '할일별 프로젝트에 옮기', 'Move to another project' => '다른 프로젝트로 이동', @@ -370,37 +364,36 @@ return array( '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 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' => '', + 'Assigned to %s with an estimate of %s/%sh' => '담당자 %s에게 예상 %s/%sh을 할당했습니다', + 'Not assigned, estimate of %sh' => '예상 %sh가 할당되지 않았습니다', '%s updated a comment on the task %s' => '%s이 할일%s의 댓글을 수정했습니다', '%s commented the task %s' => '%s이 할일%s에 댓글을 남겼습니다', '%s\'s activity' => '%s의 활동', 'RSS feed' => 'RSS피드', - // '%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 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 moved the task #%d to the position %d in the column "%s"' => '%s이 할일#%d을 칼럼 "%s"의 %d 위치로 이동시켰습니다', 'Activity' => '활동', - // 'Default values are "%s"' => '', - // 'Default columns for new projects (Comma-separated)' => '', + '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 change the assignee of the task #%d to %s' => '%s이 할일 #%d의 담당을 %s로 변경합니다', '%s changed the assignee of the task %s to %s' => '%s이 할일 %s의 담당을 %s로 변경했습니다', 'New password for the user "%s"' => '사용자 "%s"의 새로운 패스워드', 'Choose an event' => '행사의 선택', @@ -413,9 +406,7 @@ return array( 'About' => '정보', 'Database driver:' => '데이터베이스 드라이버:', 'Board settings' => '기본 설정', - 'URL and token' => 'URL와 토큰', 'Webhook settings' => 'Webhook의 설정', - 'URL for task creation:' => 'Task작성의 URL:', 'Reset token' => '토큰 리셋', 'API endpoint:' => 'API엔드 포인트:', 'Refresh interval for private board' => '비공개 보드의 갱신 빈도', @@ -426,8 +417,8 @@ return array( // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', 'Application URL' => '애플리케이션의 URL', 'Token regenerated.' => '토큰이 다시 생성되었습니다.', - 'Date format' => '데이터 포맷', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', + 'Date format' => '데이터 포멧', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO 포멧은 항상 가능합니다. 예를 들어: "%s" 와 "%s"', 'New private project' => '새 비공개 프로젝트', 'This project is private' => '이 프로젝트는 비공개입니다', 'Add' => '추가', @@ -452,12 +443,12 @@ return array( 'Number of tasks' => '할일 수', 'Task distribution' => '할일 분포', 'Reportings' => '리포트', - // 'Task repartition for "%s"' => '', + 'Task repartition for "%s"' => '"%s"의 할일 분포', 'Analytics' => '분석', 'Subtask' => '서브 할일', 'My subtasks' => '내 서브 할일', 'User repartition' => '담당자 분포', - // 'User repartition for "%s"' => '', + 'User repartition for "%s"' => '"%s"의 담당자 분포', 'Clone this project' => '이 프로젝트를 복제하는 ', 'Column removed successfully.' => '(※)컬럼을 삭제했습니다', 'Not enough data to show the graph.' => '그래프를 선묘화하려면 나왔지만 부족합니다', @@ -473,23 +464,22 @@ return array( '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"' => '', + '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.' => '이 출력은 날짜의 칼람별 할일 수를 집계한 것입니다', 'Active swimlanes' => '액티브한 스윔레인', 'Add a new swimlane' => ' 새로운 스윔레인', 'Change default swimlane' => '기본 스윔레인의 변경', 'Default swimlane' => '기본 스윔레인', - // 'Do you really want to remove this swimlane: "%s"?' => '', + 'Do you really want to remove this swimlane: "%s"?' => '스웜레인을 삭제하시겠습니까: "%s"?', 'Inactive swimlanes' => '인터랙티브한 스윔레인', 'Remove a swimlane' => '스윔레인의 삭제', 'Show default swimlane' => '기본 스윔레인의 표시', - // 'Swimlane modification for the project "%s"' => '', - 'Swimlane not found.' => '스윔레인이 발견되지 않습니다.', + 'Swimlane modification for the project "%s"' => '"%s" 프로젝트의 스웜레인 수정', 'Swimlane removed successfully.' => '스윔레인을 삭제했습니다.', 'Swimlanes' => '스윔레인', 'Swimlane updated successfully.' => '스윔레인을 갱신했습니다.', @@ -497,14 +487,14 @@ return array( '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)' => '', + 'Example: "Bug, Feature Request, Improvement"' => '예: "버그, 특성 요청, 향상"', + 'Default categories for new projects (Comma-separated)' => '새로운 프로젝트의 기본 카테고리 (콤마(,)로 구분)', 'Integrations' => '연계', 'Integration with third-party services' => '외부 서비스 연계', 'Subtask Id' => '서브 할일 Id', 'Subtasks' => '서브 할일', 'Subtasks Export' => '서브 할일 출력', - // 'Subtasks exportation for "%s"' => '', + 'Subtasks exportation for "%s"' => '"%s"의 서브 할일 출력', 'Task Title' => '할일 제목', 'Untitled' => '제목 없음', 'Application default' => '애플리케이션 기본', @@ -516,26 +506,25 @@ return array( '#%d' => '#%d', 'All swimlanes' => '모든 스윔레인', 'All colors' => '모든 색', - // 'Moved to column %s' => '', - 'Change description' => '설명 수정', + 'Moved to column %s' => '"%s" 칼럼으로 이동', 'User dashboard' => '대시보드', 'Allow only one subtask in progress at the same time for a user' => '한 사용자에 대한 하나의 할일만 진행 중에 가능합니다', - // 'Edit column "%s"' => '', - // 'Select the new status of the subtask: "%s"' => '', + 'Edit column "%s"' => '"%s" 칼럼 수정', + 'Select the new status of the subtask: "%s"' => '서브 할일의 새로운 상태 선택: "%s"', 'Subtask timesheet' => '서브 할일 타임시트', 'There is nothing to show.' => '기록이 없습니다', - 'Time Tracking' => '타임 트레킹', + 'Time Tracking' => '시간 트레킹', 'You already have one subtask in progress' => '이미 진행 중인 서브 할일가 있습니다.', 'Which parts of the project do you want to duplicate?' => '프로젝트의 무엇을 복제합니까?', - // 'Disallow login form' => '', + 'Disallow login form' => '허가되지 않은 로그인 양식', 'Start' => '시작', 'End' => '종료', 'Task age in days' => '할일이 생긴 시간', 'Days in this column' => '이 칼럼에 있는 시간', '%dd' => '%d일', 'Add a new link' => ' 새로운 링크 추가', - // 'Do you really want to remove this link: "%s"?' => '', - // 'Do you really want to remove this link with task #%d?' => '', + '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.' => '링크를 갱신했습니다.', @@ -609,7 +598,7 @@ return array( 'Change reference currency' => '현재의 기축 통화', 'Add a new currency rate' => ' 새로운 통화 환율을 추가', 'Reference currency' => '기축 통화', - // 'The currency rate have been added successfully.' => '', + 'The currency rate have been added successfully.' => '통화가 성공적으로 추가되었습니다', 'Unable to add this currency rate.' => '이 통화 환율을 추가할 수 없습니다.', 'Webhook URL' => 'Webhook URL', '%s remove the assignee of the task %s' => '%s이 할일 %s의 담당을 삭제했습니다', @@ -620,35 +609,35 @@ return array( 'The two factor authentication code is valid.' => '2단 인증 코드는 유효합니다.', 'Code' => '코드', 'Two factor authentication' => '2단 인증', - // 'This QR code contains the key URI: ' => '', + 'This QR code contains the key URI: ' => 'QR 코드는 키 URI를 포함합니다: ', 'Check my code' => '코드 체크', - // 'Secret key: ' => '', + 'Secret key: ' => '비밀키: ', 'Test your device' => '디바이스 테스트', - // 'Assign a color when the task is moved to a specific column' => '', + 'Assign a color when the task is moved to a specific column' => '상세 칼럼으로 이동할 할일의 색깔을 지정하세요', '%s via Kanboard' => '%s via E-board', - // 'Burndown chart for "%s"' => '', - // 'Burndown chart' => '', + 'Burndown chart for "%s"' => '"%s" 번다운 차트', + 'Burndown chart' => '번다운 차트', // 'This chart show the task complexity over the time (Work Remaining).' => '', 'Screenshot taken %s' => '스크린샷_%s', 'Add a screenshot' => '스크린샷 추가', 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '스크린샷을 CTRL+V 혹은 ⌘+V를 눌러 붙여넣기', 'Screenshot uploaded successfully.' => '스크린샷을 업로드하였습니다', // 'SEK - Swedish Krona' => '', - // 'Identifier' => '', - // 'Disable two factor authentication' => '', - // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', - // 'Edit link' => '', - 'Start to type task title...' => '할일 제목을 처음부터 입력해주세요', - // 'A task cannot be linked to itself' => '', - // 'The exact same link already exists' => '', + 'Identifier' => '식별자', + 'Disable two factor authentication' => '이중 인증 비활성화', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => '"%s" 담당자의 이중 인증을 비활성화 하시겠습니까?', + 'Edit link' => '링크 수정', + 'Start to type task title...' => '할일 제목을 입력하세요', + 'A task cannot be linked to itself' => '할일을 자기자신에게 연결할 수 없습니다', + 'The exact same link already exists' => '동일한 링크가 이미 존재합니다', // 'Recurrent task is scheduled to be generated' => '', - // 'Score' => '', - // 'The identifier must be unique' => '', - // 'This linked task id doesn\'t exists' => '', - // 'This value must be alphanumeric' => '', + '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' => '반복 할일 트리거 만들기', + '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' => '새로운 기본 종료날짜 계산', @@ -659,36 +648,36 @@ return array( 'Existing due date' => '기존 종료날짜', 'Factor to calculate new due date: ' => '새로운 종료날짜 계산: ', 'Month(s)' => '월', - // 'Recurrence' => '', - // 'This task has been created by: ' => '', - // 'Recurrent task has been generated:' => '', + 'Recurrence' => '반복', + 'This task has been created by: ' => '할일을 만들었습니다: ', + 'Recurrent task has been generated:' => '반복 할일이 생성되었습니다', 'Timeframe to calculate new due date: ' => '종료날짜 계산 단위', // 'Trigger to generate recurrent task: ' => '', 'When task is closed' => '할일을 마쳤을때', 'When task is moved from first column' => '할일이 첫번째 칼럼으로 옮겨졌을때', 'When task is moved to last column' => '할일이 마지막 칼럼으로 옮겨졌을때', 'Year(s)' => '년', - // 'Calendar settings' => '', - // 'Project calendar view' => '', + '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' => '', + 'Show subtasks based on the time tracking' => '시간 트래킹의 서브 할일 보기', + 'Show tasks based on the creation date' => '생성 날짜로 할일 보기', + 'Show tasks based on the start date' => '시작 날짜로 할일 보기', + 'Subtasks time tracking' => '서브 할일 시간 트래킹', + 'User calendar view' => '담당자 달력 보기', + 'Automatically update the start date' => '시작일에 자동 업데이트', // 'iCal feed' => '', - // 'Preferences' => '', - // 'Security' => '', - 'Two factor authentication disabled' => '2단 인증 비활성화', - // 'Two factor authentication enabled' => '', - // 'Unable to update this user.' => '', - // 'There is no user management for private projects.' => '', - // 'User that will receive the email' => '', + 'Preferences' => '우선권', + 'Security' => '보안', + 'Two factor authentication disabled' => '이중 인증이 비활성화 되었습니다', + 'Two factor authentication enabled' => '이중 인증이 활성화 되었습니다', + 'Unable to update this user.' => '담당자의 업데이트가 가능합니다', + 'There is no user management for private projects.' => '비밀 프로젝트의 관리 담당자가 없습니다', + 'User that will receive the email' => '그 담당자가 이메일을 수신할 것입니다', 'Email subject' => '이메일 제목', 'Date' => '날짜', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', + 'Add a comment log when moving the task between columns' => '칼럼 중간의 할일이 이동할 때 의견 달기', + 'Move the task to another column when the category is changed' => '카테고리 변경시 할일을 다른 칼럼으로 이동', 'Send a task by email to someone' => '할일을 이메일로 보내기', 'Reopen a task' => '할일 다시 시작', 'Column change' => '칼럼 이동', @@ -697,19 +686,18 @@ return array( 'Assignee change' => '담당자 변경', '[%s] Overdue tasks' => '[%s] 마감시간 지남', 'Notification' => '알림', - // '%s moved the task #%d to the first swimlane' => '', - // '%s moved the task #%d to the swimlane "%s"' => '', + '%s moved the task #%d to the first swimlane' => '%s가 할일 #%d를 첫번째 스웜레인으로 이동시켰습니다', + '%s moved the task #%d to the swimlane "%s"' => '%s가 할일 #%d를 "%s" 스웜레인으로 이동시켰습니다', 'Swimlane' => '스윔레인', // 'Gravatar' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', - // 'This report contains all subtasks information for the given date range.' => '', - // 'This report contains all tasks information for the given date range.' => '', - // 'Project activities for %s' => '', - // 'view the board on Kanboard' => '', - // 'The task have been moved to the first swimlane' => '', - // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', + '%s moved the task %s to the first swimlane' => '%s가 할일 %s를 첫번째 스웜레인으로 이동시켰습니다', + '%s moved the task %s to the swimlane "%s"' => '%s가 할일 %s를 %s 스웜레인으로 이동시켰습니다', + 'This report contains all subtasks information for the given date range.' => '해당 기간의 모든 서브 할일 정보가 보고서에 포함됩니다', + 'This report contains all tasks information for the given date range.' => '해당 기간의 모든 할일 정보가 보고서에 포함됩니다', + 'Project activities for %s' => '%s의 프로젝트 활성화', + 'view the board on Kanboard' => 'kanboard로 보드 보기', + 'The task have been moved to the first swimlane' => '할일이 첫번째 스웜라인으로 이동되어 있습니다', + 'The task have been moved to another swimlane:' => '할일이 다른 스웜라인으로 이동되어 있습니다', 'New title: %s' => '제목 변경: %s', 'The task is not assigned anymore' => '담당자 없음', 'New assignee: %s' => '담당자 변경: %s', @@ -722,8 +710,8 @@ return array( 'Recurrence settings have been modified' => '반복할일 설정 수정', 'Time spent changed: %sh' => '경과시간 변경: %s시간', 'Time estimated changed: %sh' => '%s시간으로 예상시간 변경', - // 'The field "%s" have been updated' => '', - // 'The description has been modified:' => '', + 'The field "%s" have been updated' => '%s 필드가 업데이트 되어있습니다', + 'The description has been modified:' => '설명이 수정되어 있습니다: ', 'Do you really want to close the task "%s" as well as all subtasks?' => '할일 "%s"과 서브 할일을 모두 마치시겠습니까?', 'I want to receive notifications for:' => '다음의 알림을 받기를 원합니다:', 'All tasks' => '모든 할일', @@ -731,17 +719,16 @@ return array( 'Only for tasks created by me' => '내가 만든 일', 'Only for tasks created by me and assigned to me' => '내가 만들었거나 내가 담당자인 일', // '%%Y-%%m-%%d' => '', - // 'Total for all columns' => '', - // 'You need at least 2 days of data to show the chart.' => '', + 'Total for all columns' => '모든 칼럼', + 'You need at least 2 days of data to show the chart.' => '차트를 보기 위하여 최소 2일의 데이터가 필요합니다', '<15m' => '<15분', '<30m' => '<30분', - // 'Stop timer' => '', + 'Stop timer' => '타이머 정지', 'Start timer' => '타이머 시작', - // 'Add project member' => '', - 'Enable notifications' => '알림 켜기', + 'Add project member' => '프로젝트 맴버 추가', 'My activity stream' => '내 활동기록', 'My calendar' => '내 캘린더', - // 'Search tasks' => '', + 'Search tasks' => '할일 찾기', 'Reset filters' => '필터 리셋', 'My tasks due tomorrow' => '내일까지 내 할일', 'Tasks due today' => '오늘까지 할일', @@ -752,16 +739,16 @@ return array( 'Not assigned' => '담당자가 없는 일', 'View advanced search syntax' => '추가 검색 문법보기', 'Overview' => '개요', - // 'Board/Calendar/List view' => '', - // 'Switch to the board view' => '', - // 'Switch to the calendar view' => '', - // 'Switch to the list view' => '', - // 'Go to the search/filter box' => '', + 'Board/Calendar/List view' => '보드/달력/리스트 보기', + 'Switch to the board view' => '보드 보기로 전환', + 'Switch to the calendar view' => '달력 보기로 전환', + 'Switch to the list view' => '리스트 보기로 전환', + 'Go to the search/filter box' => '검색/필터 박스로 이동', 'There is no activity yet.' => '활동이 없습니다', - // 'No tasks found.' => '', - // 'Keyboard shortcut: "%s"' => '', + 'No tasks found.' => '할일을 찾을 수 없습니다', + 'Keyboard shortcut: "%s"' => '쉬운 키보드: "%s"', 'List' => '목록', - // 'Filter' => '', + 'Filter' => '필터', 'Advanced search' => '검색 문법', 'Example of query: ' => '문법 예제 ', 'Search by project: ' => '프로젝트로 찾기 ', @@ -771,58 +758,58 @@ return array( 'Search by category: ' => '카테고리로 찾기 ', 'Search by description: ' => '설명으로 찾기 ', 'Search by due date: ' => '마감날짜로 찾기 ', - // 'Lead and Cycle time for "%s"' => '', - // 'Average time spent into each column for "%s"' => '', - // 'Average time spent into each column' => '', - // 'Average time spent' => '', - // 'This chart show the average time spent into each column for the last %d tasks.' => '', - // 'Average Lead and Cycle time' => '', - // 'Average lead time: ' => '', - // 'Average cycle time: ' => '', - 'Cycle Time' => '사이클 타임', - 'Lead Time' => '리드 타임', - // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', - // 'Average time into each column' => '', - // 'Lead and cycle time' => '', - 'Lead time: ' => '리드 타임: ', - 'Cycle time: ' => '사이클 타임: ', + 'Lead and Cycle time for "%s"' => '"%s"의 리드와 사이클 시간', + 'Average time spent into each column for "%s"' => '"%s"의 각 칼럼 평균 소요시간', + 'Average time spent into each column' => '각 칼럼의 평균 소요시간', + 'Average time spent' => '평균 소요시간', + 'This chart show the average time spent into each column for the last %d tasks.' => '마지막 %d 할일의 칼럼 평균 소요시간을 차트에 표시합니다', + 'Average Lead and Cycle time' => '평균 Lead and Cycle 시간', + 'Average lead time: ' => '평균 lead 시간', + 'Average cycle time: ' => '평균 cycle 시간', + 'Cycle Time' => '사이클 시간', + 'Lead Time' => '리드 시간', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '마지막 %d 할일의 평균 리드와 사이클 시간을 차트에 표시합니다', + 'Average time into each column' => '각 칼럼의 평균 시간', + 'Lead and cycle time' => '리드와 사이클 시간', + 'Lead time: ' => '리드 시간: ', + 'Cycle time: ' => '사이클 시간: ', 'Time spent into each column' => '각 칼럼에서 걸린 시간', - // 'The lead time is the duration between the task creation and the completion.' => '', - // 'The cycle time is the duration between the start date and the completion.' => '', - // 'If the task is not closed the current time is used instead of the completion date.' => '', - // 'Set automatically the start date' => '', + 'The lead time is the duration between the task creation and the completion.' => '리드 시간은 할일의 생성부터 완료까지의 기간입니다', + 'The cycle time is the duration between the start date and the completion.' => '사이클 시간은 할일의 시작일부터 완료까지의 기간입니다', + 'If the task is not closed the current time is used instead of the completion date.' => '할일이 종료되지 않았다면, 완료 시간 대신 현재 시간이 사용됩니다', + 'Set automatically the start date' => '자동으로 시작 날짜를 설정합니다', 'Edit Authentication' => '계정 수정', - // 'Remote user' => '', - // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', - // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', + 'Remote user' => '원격 담당자', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '예를 들어 LDAP, Google, Github 계정같은 원격 담당자의 비밀번호는 칸반보드 데이터베이스에 저장하지 않습니다', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '만약 "로그인 폼 거절"에 체크한다면, 로그인 폼에 접근할 자격이 무시됩니다', 'New remote user' => '새로운 원격유저', 'New local user' => '새로운 유저', - // 'Default task color' => '', + 'Default task color' => '기본 할일 색', 'This feature does not work with all browsers.' => '이 기능은 일부 브라우저에서 작동하지 않습니다', - // 'There is no destination project available.' => '', - // 'Trigger automatically subtask time tracking' => '', - // 'Include closed tasks in the cumulative flow diagram' => '', - // 'Current swimlane: %s' => '', - // 'Current column: %s' => '', - // 'Current category: %s' => '', - // 'no category' => '', - // 'Current assignee: %s' => '', - // 'not assigned' => '', - // 'Author:' => '', - // 'contributors' => '', - // 'License:' => '', - // 'License' => '', - // 'Enter the text below' => '', - // 'Gantt chart for %s' => '', - // 'Sort by position' => '', - // 'Sort by date' => '', - // 'Add task' => '', - // 'Start date:' => '', - // 'Due date:' => '', - // 'There is no start date or due date for this task.' => '', + 'There is no destination project available.' => '가능한 목적 프로젝트가 없습니다', + 'Trigger automatically subtask time tracking' => '자동 서브 할일 시간 트래킹 트리거', + 'Include closed tasks in the cumulative flow diagram' => '누적 플로우 다이어그램에 종료된 할일을 포함합니다', + 'Current swimlane: %s' => '현재 스웜라인: %s', + 'Current column: %s' => '현재 칼럼: %s', + 'Current category: %s' => '현재 카테고리: %s', + 'no category' => '카테고리 아님', + 'Current assignee: %s' => '현재 할당자: %s', + 'not assigned' => '할당되지 않음', + 'Author:' => '글쓴이:', + 'contributors' => '기여자', + 'License:' => '라이센스:', + 'License' => '라이센스', + 'Enter the text below' => '아랫글로 들어가기', + 'Gantt chart for %s' => '%s의 간트 차트', + 'Sort by position' => '위치별 정렬', + 'Sort by date' => '날짜별 정렬', + 'Add task' => '할일 추가', + 'Start date:' => '시작일:', + 'Due date:' => '만기일:', + 'There is no start date or due date for this task.' => '할일의 시작일 또는 만기일이 없습니다', // 'Moving or resizing a task will change the start and due date of the task.' => '', // 'There is no task in your project.' => '', - 'Gantt chart' => '간트차트', + 'Gantt chart' => '간트 차트', 'People who are project managers' => '프로젝트 매니저', 'People who are project members' => '프로젝트 멤버', // 'NOK - Norwegian Krone' => '', @@ -832,39 +819,39 @@ return array( 'End date' => '종료 날짜', 'Users overview' => '유저 전체보기', 'Members' => '멤버', - // 'Shared project' => '', + 'Shared project' => '프로젝트 공유', 'Project managers' => '프로젝트 매니저', - // 'Gantt chart for all projects' => '', + 'Gantt chart for all projects' => '모든 프로젝트의 간트 차트', 'Projects list' => '프로젝트 리스트', 'Gantt chart for this project' => '이 프로젝트 간트차트', 'Project board' => '프로젝트 보드', - // 'End date:' => '', + 'End date:' => '날짜 수정', 'There is no start date or end date for this project.' => '이 프로젝트에는 시작날짜와 종료날짜가 없습니다', 'Projects Gantt chart' => '프로젝트 간트차트', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Milestone' => '', - // 'Documentation: %s' => '', - // 'Switch to the Gantt chart view' => '', + 'Milestone' => '마일스톤', + 'Documentation: %s' => '문서: %s', + 'Switch to the Gantt chart view' => '간트 차트 보기로 변경', // 'Reset the search/filter box' => '', - // 'Documentation' => '', + 'Documentation' => '문서', // 'Table of contents' => '', 'Gantt' => '간트', - // 'Author' => '', - // 'Version' => '', - // 'Plugins' => '', - // 'There is no plugin loaded.' => '', + 'Author' => '글쓴이', + 'Version' => '버전', + 'Plugins' => '플러그인', + 'There is no plugin loaded.' => '플러그인이 로드되지 않았습니다', 'Set maximum column height' => '최대 칼럼 높이 제한하기', 'Remove maximum column height' => '최대 칼럼 높이 없애기', 'My notifications' => '내 알림', 'Custom filters' => '사용자 정의 필터', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', + 'Your custom filter have been created successfully.' => '사용자 정의 필터가 성공적으로 생성되었습니다', + 'Unable to create your custom filter.' => '사용자 정의 필터 생성 비활성화', + 'Custom filter removed successfully.' => '사용자 정의 필터가 성공적으로 삭제되었습니다', + 'Unable to remove this custom filter.' => '정의 필터 삭제 비활성화', + 'Edit custom filter' => '정의 필터 수정', + 'Your custom filter have been updated successfully.' => '사용자 정의 필터가 성공적으로 수정되었습니다', + 'Unable to update custom filter.' => '정의 필터 수정 비활성화', 'Web' => '웹', // 'New attachment on task #%d: %s' => '', 'New comment on task #%d' => '할일 #%d에 새로운 댓글이 달렸습니다', @@ -874,283 +861,359 @@ return array( 'New task #%d: %s' => '할일 #%d: %s이 추가되었습니다', 'Task updated #%d' => '할일 #%d이 업데이트되었습니다', 'Task #%d closed' => '할일 #%d를 마쳤습니다', - // 'Task #%d opened' => '', + 'Task #%d opened' => '할일 #%d가 시작되었습니다', 'Column changed for task #%d' => '할일 #%d의 칼럼이 변경되었습니다', 'New position for task #%d' => '할일 #%d이 새로운 위치에 등록되었습니다', - // 'Swimlane changed for task #%d' => '', - // 'Assignee changed on task #%d' => '', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', + 'Swimlane changed for task #%d' => '#%d 할일의 스웜라인이 변경됩니다', + 'Assignee changed on task #%d' => '#%d 할일의 담당자가 변경됩니다', + '%d overdue tasks' => '할일의 기한이 %d일 지났습니다', + 'Task #%d is overdue' => '#%d 할일의 기한이 지났습니다', 'No new notifications.' => '알림이 없습니다', 'Mark all as read' => '모두 읽음', 'Mark as read' => '읽음', // 'Total number of tasks in this column across all swimlanes' => '', - // 'Collapse swimlane' => '', - // 'Expand swimlane' => '', - // 'Add a new filter' => '', - // 'Share with all project members' => '', - // 'Shared' => '', - // 'Owner' => '', + 'Collapse swimlane' => '스웜라인 붕괴', + 'Expand swimlane' => '스웜라인 확장', + 'Add a new filter' => '새로운 필터 추가', + 'Share with all project members' => '모든 프로젝트 맴버 공유', + 'Shared' => '공유', + 'Owner' => '소유자', 'Unread notifications' => '읽지않은 알림', 'Notification methods:' => '알림 방법', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', - // 'Comma' => '', - // 'Semi-colon' => '', - // 'Tab' => '', - // 'Vertical bar' => '', - // 'Double Quote' => '', - // 'Single Quote' => '', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', + 'Import tasks from CSV file' => 'CSV 파일에서 할일 가져오기', + 'Unable to read your file' => '파일을 읽을 수 없습니다', + '%d task(s) have been imported successfully.' => '%d 할일이 성공적으로 추가되었습니다', + 'Nothing have been imported!' => '추가가 되지 않았습니다!', + 'Import users from CSV file' => 'CSV 파일에서 사용자 가져오기', + '%d user(s) have been imported successfully.' => '%d 사용자가 성공적으로 추가되었습니다', + 'Comma' => '콤마', + 'Semi-colon' => '세미콜론', + 'Tab' => '탭', + 'Vertical bar' => '세로줄', + 'Double Quote' => '이중 따옴표', + 'Single Quote' => '따옴표', + '%s attached a file to the task #%d' => '%s가 할일 #%d에 파일을 추가하였습니다', + 'There is no column or swimlane activated in your project!' => '프로젝트에 활성화된 칼럼이나 스웜라인이 없습니다', // 'Append filter (instead of replacement)' => '', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', + 'Append/Replace' => '추가/변경', + 'Append' => '추가', + 'Replace' => '변경', 'Import' => '가져오기', - // 'change sorting' => '', - // 'Tasks Importation' => '', - // 'Delimiter' => '', - // 'Enclosure' => '', - // 'CSV File' => '', - // 'Instructions' => '', - // 'Your file must use the predefined CSV format' => '', - // 'Your file must be encoded in UTF-8' => '', - // 'The first row must be the header' => '', - // 'Duplicates are not verified for you' => '', - // 'The due date must use the ISO format: YYYY-MM-DD' => '', - // 'Download CSV template' => '', + 'change sorting' => '정렬 변경', + 'Tasks Importation' => '할일 가져오기', + 'Delimiter' => '구분자', + 'Enclosure' => '동봉', + 'CSV File' => 'CSV 파일', + 'Instructions' => '명령', + 'Your file must use the predefined CSV format' => '파일은 미리 정의된 CVS 형식이어야 합니다', + 'Your file must be encoded in UTF-8' => '파일은 UTF-8로 인코딩되어야 합니다', + 'The first row must be the header' => '첫 줄은 헤더이어야 합니다', + 'Duplicates are not verified for you' => '사본이 허락되지 않습니다', + 'The due date must use the ISO format: YYYY-MM-DD' => '만기일은 ISO 형식이어야 합니다: YYYY-MM-DD', + 'Download CSV template' => 'CVS 탬플릿 내려받기', 'No external integration registered.' => '설정이 되어있지 않습니다', - // 'Duplicates are not imported' => '', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', + 'Duplicates are not imported' => '사본을 가져올 수 없습니다', + 'Usernames must be lowercase and unique' => '사용자 이름은 소문자이며 유일해야 합니다', + 'Passwords will be encrypted if present' => '비밀번호는 암호화됩니다', '%s attached a new file to the task %s' => '%s이 새로운 파일을 할일 %s에 추가했습니다', - // 'Link type' => '', - // 'Assign automatically a category based on a link' => '', + 'Link type' => '링크 형식', + 'Assign automatically a category based on a link' => '링크 기반 자동 카테고리 할당', // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', - // 'Groups' => '', - // 'Members of %s' => '', - // 'New group' => '', - // 'Group created successfully.' => '', - // 'Unable to create your group.' => '', - // 'Edit group' => '', - // 'Group updated successfully.' => '', - // 'Unable to update your group.' => '', - // 'Add group member to "%s"' => '', - // 'Group member added successfully.' => '', - // 'Unable to add group member.' => '', - // 'Remove user from group "%s"' => '', - // 'User removed successfully from this group.' => '', - // 'Unable to remove this user from the group.' => '', - // 'Remove group' => '', - // 'Group removed successfully.' => '', - // 'Unable to remove this group.' => '', - // 'Project Permissions' => '', + 'Assignee Username' => '담당자의 사용자이름', + 'Assignee Name' => '당장자 이름', + 'Groups' => '그룹', + 'Members of %s' => '%s의 맴버', + 'New group' => '새로운 그룹', + 'Group created successfully.' => '그룹이 성공적으로 생성되었습니다', + 'Unable to create your group.' => '그룹 생성 비활성화', + 'Edit group' => '그룹 편집', + 'Group updated successfully.' => '그룹이 성공적으로 수정되었습니다', + 'Unable to update your group.' => '그룹 수정 비활성화', + 'Add group member to "%s"' => '%s 그룹 맴버 추가', + 'Group member added successfully.' => '그룹 맴버가 성공적으로 추가되었습니다', + 'Unable to add group member.' => '그룹 맴버 추가 비활성화', + 'Remove user from group "%s"' => '%s 그룹 사용자 삭제', + 'User removed successfully from this group.' => '그룹 사용자가 성공적으로 삭제되었습니다', + 'Unable to remove this user from the group.' => '그룹 사용자 삭제 비활성화', + 'Remove group' => '그룹 삭제', + 'Group removed successfully.' => '그룹이 성공적으로 삭제되었습니다', + 'Unable to remove this group.' => '그룹 삭제 비활성화', + 'Project Permissions' => '프로젝트 권한', 'Manager' => '매니저', 'Project Manager' => '프로젝트 매니저', 'Project Member' => '프로젝트 멤버', - // 'Project Viewer' => '', - // 'Your account is locked for %d minutes' => '', + 'Project Viewer' => '프로젝트 뷰어', + 'Your account is locked for %d minutes' => '%d분 동안 계정이 잠깁니다', // 'Invalid captcha' => '', - // 'The name must be unique' => '', + 'The name must be unique' => '이름은 유일해야 합니다', 'View all groups' => '모든그룹보기', - // 'View group members' => '', - // 'There is no user available.' => '', - // 'Do you really want to remove the user "%s" from the group "%s"?' => '', - // 'There is no group.' => '', - // 'External Id' => '', + 'View group members' => '그룹맴버 보기', + 'There is no user available.' => '가능한 사용자가 없습니다', + 'Do you really want to remove the user "%s" from the group "%s"?' => '"%s" 사용자를 "%s" 에서 삭제하시겠습니까?', + 'There is no group.' => '그룹이 없습니다', + 'External Id' => '외부 아이디', 'Add group member' => '멤버추가', - // 'Do you really want to remove this group: "%s"?' => '', - // 'There is no user in this group.' => '', - // 'Remove this user' => '', + 'Do you really want to remove this group: "%s"?' => '그룹을 삭제하시겠습니까: "%s"?', + 'There is no user in this group.' => '이 그룹에는 사용자가 없습니다', + 'Remove this user' => '사용자 삭제', 'Permissions' => '권한', - // 'Allowed Users' => '', - // 'No user have been allowed specifically.' => '', + 'Allowed Users' => '사용자 승인', + 'No user have been allowed specifically.' => '구체적으로 승인된 사용자가 없습니다', 'Role' => '역할', - // 'Enter user name...' => '', - // 'Allowed Groups' => '', - // 'No group have been allowed specifically.' => '', - // 'Group' => '', - // 'Group Name' => '', - // 'Enter group name...' => '', + 'Enter user name...' => '사용자 이름을 입력합니다...', + 'Allowed Groups' => '승인된 그룹', + 'No group have been allowed specifically.' => '구체적으로 승인된 그룹이 없습니다', + 'Group' => '그룹', + 'Group Name' => '그룹명', + 'Enter group name...' => '그룹명을 입력합니다...', 'Role:' => '역할: ', 'Project members' => '프로젝트 멤버', - // 'Compare hours for "%s"' => '', - // '%s mentioned you in the task #%d' => '', - // '%s mentioned you in a comment on the task #%d' => '', - // 'You were mentioned in the task #%d' => '', + 'Compare hours for "%s"' => '"%s" 시간동안 비교', + '%s mentioned you in the task #%d' => '#%d 할일에서 %s가 당신을 언급하였습니다', + '%s mentioned you in a comment on the task #%d' => '#%d 할일에서 %s가 당신의 댓글을 언급하였습니다', + 'You were mentioned in the task #%d' => '#%d 할일에서 당신이 언급되었습니다', 'You were mentioned in a comment on the task #%d' => '할일 #%d의 댓글에서 언급되었습니다', - // 'Mentioned' => '', - // 'Compare Estimated Time vs Actual Time' => '', - // 'Estimated hours: ' => '', - // 'Actual hours: ' => '', - // 'Hours Spent' => '', - // 'Hours Estimated' => '', - // 'Estimated Time' => '', - // 'Actual Time' => '', - // 'Estimated vs actual time' => '', + 'Mentioned' => '언급된', + 'Compare Estimated Time vs Actual Time' => '예상 시간과 실제 시간 비교', + 'Estimated hours: ' => '예상 시간: ', + 'Actual hours: ' => '실제 시간: ', + 'Hours Spent' => '소요 시간', + 'Hours Estimated' => '예상 시간', + 'Estimated Time' => '예상 시간', + 'Actual Time' => '실제 시간', + 'Estimated vs actual time' => '예상 vs 실제 시간', // 'RUB - Russian Ruble' => '', - // 'Assign the task to the person who does the action when the column is changed' => '', - // 'Close a task in a specific column' => '', + 'Assign the task to the person who does the action when the column is changed' => '칼럼이 변경되면 액션하지 않는 사람에게 할일을 할당합니다', + 'Close a task in a specific column' => '상세 칼럼의 할일을 종료합니다', 'Time-based One-time Password Algorithm' => '시간에 기반한 1회용 패스워드 알고리즘', - 'Two-Factor Provider: ' => '2단 인증: ', - // 'Disable two-factor authentication' => '', - 'Enable two-factor authentication' => '2단 인증 활성화', + 'Two-Factor Provider: ' => '이중 인증: ', + 'Disable two-factor authentication' => '이중 인증 비활성화', + 'Enable two-factor authentication' => '이중 인증 활성화', // 'There is no integration registered at the moment.' => '', - // 'Password Reset for Kanboard' => '', + 'Password Reset for Kanboard' => 'Kanboard의 비밀번호 초기화', 'Forgot password?' => '비밀번호 찾기', - // 'Enable "Forget Password"' => '', - // 'Password Reset' => '', - // 'New password' => '', - // 'Change Password' => '', - // 'To reset your password click on this link:' => '', - 'Last Password Reset' => '비밀번호 초기화', + 'Enable "Forget Password"' => '"비밀번호 분실" 활성화', + 'Password Reset' => '비밀번호 초기화', + 'New password' => '새로운 비밀번호', + 'Change Password' => '비밀번호 변경', + 'To reset your password click on this link:' => '비밀번호를 초기화 하시려면 링크를 눌러주세요:', + 'Last Password Reset' => '마지막 비밀번호 초기화', 'The password has never been reinitialized.' => '비밀번호가 초기화되지 않았습니다', - // 'Creation' => '', - // 'Expiration' => '', + 'Creation' => '생성', + 'Expiration' => '만료', 'Password reset history' => '비밀번호 초기화 기록', - // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', - // 'Do you really want to close all tasks of this column?' => '', - // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '칼럼 "%s"와 스웜라인 "%s"의 모든 할일이 성공적으로 종료되었습니다', + 'Do you really want to close all tasks of this column?' => '이 칼럼의 모든 할일을 종료 하시겠습니까?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '칼럼 "%s"와 스웜라인 "%s"의 할일 %d가 종료될 것입니다', 'Close all tasks of this column' => '칼럼의 모든 할일 마치기', - // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '', + 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '프로젝트 알림 방법으로 플러그인이 등록되지 않았습니다. 각각의 알림을 프로파일에서 설정하실 수 있습니다', 'My dashboard' => '대시보드', 'My profile' => '프로필', - // 'Project owner: ' => '', - // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '', + 'Project owner: ' => '프로젝트 소유자', + 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '프로젝트 구분자는 선택사항이며, 숫자와 문자만 가능합니다. 예: MYPROJECT.', // 'Project owner' => '', - // 'Those dates are useful for the project Gantt chart.' => '', - // 'Private projects do not have users and groups management.' => '', - // 'There is no project member.' => '', - // 'Priority' => '', - // 'Task priority' => '', - // 'General' => '', - // 'Dates' => '', - // 'Default priority' => '', - // 'Lowest priority' => '', - // 'Highest priority' => '', - // 'If you put zero to the low and high priority, this feature will be disabled.' => '', - // 'Close a task when there is no activity' => '', - // 'Duration in days' => '', - // 'Send email when there is no activity on a task' => '', - // 'Unable to fetch link information.' => '', + 'Those dates are useful for the project Gantt chart.' => '이 날짜는 프로젝트 간트 차트에 사용됩니다', + 'Private projects do not have users and groups management.' => '비밀 프로젝트는 사용자나 관리 그룹이 소유하지 않습니다', + 'There is no project member.' => '프로젝트 맴버가 없습니다', + 'Priority' => '우선순위', + 'Task priority' => '할일의 우선순위', + 'General' => '일반적인', + 'Dates' => '날짜', + 'Default priority' => '기본 우선순위', + 'Lowest priority' => '낮은 우선순위', + 'Highest priority' => '높은 우선순위', + 'If you put zero to the low and high priority, this feature will be disabled.' => '만약 낮은/높은 우선순위에 0을 입력하면 이 특성은 비활성화됩니다', + 'Close a task when there is no activity' => '활동이 없는 할일을 종료합니다', + 'Duration in days' => '기간', + 'Send email when there is no activity on a task' => '활동이 없는 할일을 이메일로 보냅니다', + 'Unable to fetch link information.' => '링크 정보 가져오기 비활성화', // 'Daily background job for tasks' => '', - // 'Auto' => '', - // 'Related' => '', - // 'Attachment' => '', - // 'Title not found' => '', - // 'Web Link' => '', - // 'External links' => '', - // 'Add external link' => '', - // 'Type' => '', - // 'Dependency' => '', - // 'Add internal link' => '', - // 'Add a new external link' => '', - // 'Edit external link' => '', - // 'External link' => '', - // 'Copy and paste your link here...' => '', + 'Auto' => '자동', + 'Related' => '연관된', + 'Attachment' => '첨부', + 'Title not found' => '제목이 없습니다', + 'Web Link' => '웹 링크', + 'External links' => '외부 링크', + 'Add external link' => '외부 링크 추가', + 'Type' => '타입', + 'Dependency' => '의존', + 'Add internal link' => '내부 링크 추가', + 'Add a new external link' => '새로운 외부 링크 추가', + 'Edit external link' => '외부 링크 수정', + 'External link' => '외부 링크', + 'Copy and paste your link here...' => '여기에 링크를 복사/붙여넣기', // 'URL' => '', - // 'Internal links' => '', - // 'Assign to me' => '', - // 'Me' => '', - // 'Do not duplicate anything' => '', - // 'Projects management' => '', - // 'Users management' => '', - // 'Groups management' => '', - // 'Create from another project' => '', - // 'open' => '', - // 'closed' => '', - // 'Priority:' => '', - // 'Reference:' => '', - // 'Complexity:' => '', - // 'Swimlane:' => '', - // 'Column:' => '', - // 'Position:' => '', - // 'Creator:' => '', - // 'Time estimated:' => '', - // '%s hours' => '', - // 'Time spent:' => '', - // 'Created:' => '', - // 'Modified:' => '', - // 'Completed:' => '', - // 'Started:' => '', - // 'Moved:' => '', - // 'Task #%d' => '', - // 'Date and time format' => '', - // 'Time format' => '', - // 'Start date: ' => '', - // 'End date: ' => '', - // 'New due date: ' => '', - // 'Start date changed: ' => '', - // 'Disable private projects' => '', - // 'Do you really want to remove this custom filter: "%s"?' => '', - // 'Remove a custom filter' => '', - // 'User activated successfully.' => '', + 'Internal links' => '내부 링크', + 'Assign to me' => '나에게 할당', + 'Me' => '나', + 'Do not duplicate anything' => '복사할까요?', + 'Projects management' => '프로젝트 관리', + 'Users management' => '사용자 관리', + 'Groups management' => '그룹 관리', + 'Create from another project' => '다른 프로젝트 생성', + 'open' => '시작', + 'closed' => '종료', + 'Priority:' => '우선순위:', + 'Reference:' => '참고:', + 'Complexity:' => '복합:', + 'Swimlane:' => '스웜라인:', + 'Column:' => '칼럼:', + 'Position:' => '위치:', + 'Creator:' => '생성자:', + 'Time estimated:' => '예상 시간:', + '%s hours' => '%s 시간', + 'Time spent:' => '소요 시간:', + 'Created:' => '생성:', + 'Modified:' => '수정;', + 'Completed:' => '완료:', + 'Started:' => '시작:', + 'Moved:' => '이동:', + 'Task #%d' => '할일 #%d', + 'Date and time format' => '날짜와 시간 형식', + 'Time format' => '시간 형식', + 'Start date: ' => '시작일: ', + 'End date: ' => '종료일: ', + 'New due date: ' => '새로운 만기일: ', + 'Start date changed: ' => '변경된 시작일: ', + 'Disable private projects' => '비밀 프로젝트 비활성화', + 'Do you really want to remove this custom filter: "%s"?' => '정의 필터를 삭제하시겠습니까: "%s"?', + 'Remove a custom filter' => '정의 필터 삭제', + 'User activated successfully.' => '사용자가 성공적으로 활성화되었습니다', // 'Unable to enable this user.' => '', - // 'User disabled successfully.' => '', + 'User disabled successfully.' => '사용자가 성공적으로 비활성화되었습니다', // 'Unable to disable this user.' => '', - // 'All files have been uploaded successfully.' => '', - // 'View uploaded files' => '', - // 'The maximum allowed file size is %sB.' => '', - // 'Choose files again' => '', - // 'Drag and drop your files here' => '', - // 'choose files' => '', - // 'View profile' => '', - // 'Two Factor' => '', - // 'Disable user' => '', - // 'Do you really want to disable this user: "%s"?' => '', - // 'Enable user' => '', - // 'Do you really want to enable this user: "%s"?' => '', - // 'Download' => '', - // 'Uploaded: %s' => '', - // 'Size: %s' => '', - // 'Uploaded by %s' => '', - // 'Filename' => '', - // 'Size' => '', - // 'Column created successfully.' => '', - // 'Another column with the same name exists in the project' => '', - // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', - // 'Change column position' => '', - // 'Switch to the project overview' => '', - // 'User filters' => '', - // 'Category filters' => '', - // 'Upload a file' => '', - // 'View file' => '', - // 'Last activity' => '', - // 'Change subtask position' => '', - // 'This value must be greater than %d' => '', - // 'Another swimlane with the same name exists in the project' => '', - // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', - // 'Actions duplicated successfully.' => '', - // 'Unable to duplicate actions.' => '', - // 'Add a new action' => '', - // 'Import from another project' => '', - // 'There is no action at the moment.' => '', - // 'Import actions from another project' => '', - // 'There is no available project.' => '', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', + 'All files have been uploaded successfully.' => '모든 파일이 성공적으로 업로드되었습니다', + 'View uploaded files' => '업로드 파일 보기', + 'The maximum allowed file size is %sB.' => '업로드 파일의 최대 크기는 %sB 입니다', + 'Choose files again' => '파일 다시 선택', + 'Drag and drop your files here' => '파일을 이곳으로 끌고오기', + 'choose files' => '파일 선택', + 'View profile' => '프로파일 보기', + 'Two Factor' => '이중', + 'Disable user' => '사용자 비활성화', + 'Do you really want to disable this user: "%s"?' => '사용자를 비활성화 시키겠습니까: "%s"?', + 'Enable user' => '사용자 활성화', + 'Do you really want to enable this user: "%s"?' => '사용자를 활성화 시키겠습니까: "%s"?', + 'Download' => '내려받기', + 'Uploaded: %s' => '올리기: %s', + 'Size: %s' => '크기: %s', + 'Uploaded by %s' => '%s로 올리기', + 'Filename' => '파일 이름', + 'Size' => '크기', + 'Column created successfully.' => '칼럼이 성공적으로 생성되었습니다', + 'Another column with the same name exists in the project' => '프로젝트에 동일한 이름의 칼럼이 있습니다', + 'Default filters' => '기본 필터', + 'Your board doesn\'t have any columns!' => '보드에 칼럼이 존재하지 않습니다', + 'Change column position' => '칼럼 위치 변경', + 'Switch to the project overview' => '프로젝트 개요로 변경', + 'User filters' => '사용자 필터', + 'Category filters' => '카테고리 필터', + 'Upload a file' => '파일 업로드', + 'View file' => '파일 보기', + 'Last activity' => '마지막 행동', + 'Change subtask position' => '서브 할일 위치 변경', + 'This value must be greater than %d' => '이 값은 %d보다 커야 합니다', + 'Another swimlane with the same name exists in the project' => '프로젝트에 같은 이름의 스웜라인이 존재합니다', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '예: http://example.kanboard.net/ (절대적 URLs을 생성하는데 사용됨)', + 'Actions duplicated successfully.' => '행동이 성공적으로 복제되었습니다', + 'Unable to duplicate actions.' => '행동 복제 비활성화', + 'Add a new action' => '새로운 행동 추가', + 'Import from another project' => '다른 프로젝트에서 가져오기', + 'There is no action at the moment.' => '현재 행동이 없습니다', + 'Import actions from another project' => '다른 프로젝트에서 행동 가져오기', + 'There is no available project.' => '사용 가능한 프로젝트가 없습니다', + 'Local File' => '로컬 파일', + 'Configuration' => '구성', + 'PHP version:' => 'PHP 버전:', // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', - // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', - // 'Menu' => '', - // 'Set start date' => '', - // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'OS version:' => '운영체제 버전:', + 'Database version:' => '데이터베이스 버전:', + 'Browser:' => '브라우저:', + 'Task view' => '할일 보기', + 'Edit task' => '할일 수정', + 'Edit description' => '설명 수정', + 'New internal link' => '새로운 내부 링크', + 'Display list of keyboard shortcuts' => '키보드 숏컷 리스트 보여주기', + 'Menu' => '메뉴', + 'Set start date' => '시작일 설정', + 'Avatar' => '아바타', + 'Upload my avatar image' => '아바타 이미지 올리기', + 'Remove my image' => '이미지 삭제', + 'The OAuth2 state parameter is invalid' => 'OAuth2 상태값이 올바르지 않습니다', + 'User not found.' => '사용자를 찾을 수 없습니다', + 'Search in activity stream' => '활성 스트림 찾기', + 'My activities' => '나의 활동', + 'Activity until yesterday' => '어제까지의 활동', + 'Activity until today' => '오늘까지의 활동', + 'Search by creator: ' => '생성자로 검색: ', + 'Search by creation date: ' => '생성 날짜로 검색: ', + 'Search by task status: ' => '할일 상태로 검색: ', + 'Search by task title: ' => '할일 제목으로 검색: ', + 'Activity stream search' => '활동 스트림 검색', + 'Projects where "%s" is manager' => '"%s" 관리자의 프로젝트', + 'Projects where "%s" is member' => '"%s" 맴버의 프로젝트', + 'Open tasks assigned to "%s"' => '"%s"에게 할당된 할일 시작하기', + 'Closed tasks assigned to "%s"' => '"%s"에게 할당된 할일 종료하기', + 'Assign automatically a color based on a priority' => '우선순위로 색깔 자동 지정', + 'Overdue tasks for the project(s) "%s"' => '"%s" 프로젝트의 기한이 지난 할일', + 'Upload files' => '파일 올리기', + 'Installed Plugins' => '설치된 플러그인', + 'Plugin Directory' => '플러그인 폴더', + 'Plugin installed successfully.' => '플러그인이 성공적으로 설치 되었습니다', + 'Plugin updated successfully.' => '플러그인이 성공적으로 업데이트 되었습니다', + 'Plugin removed successfully.' => '플러그인이 성공적으로 삭제 되었습니다', + 'Subtask converted to task successfully.' => '서브 할일이 성공적으로 할일로 변경 되었습니다', + 'Unable to convert the subtask.' => '서브할일 변경 비활성화', + 'Unable to extract plugin archive.' => '플러그인 보관소 비활성화', + 'Plugin not found.' => '플러그인을 찾을 수 없습니다', + 'You don\'t have the permission to remove this plugin.' => '플러그인 삭제 권한이 없습니다', + 'Unable to download plugin archive.' => '플러그인 보관소 다운로드 비활성화', + 'Unable to write temporary file for plugin.' => '플러그인의 임시 파일 기록 비활성화', + 'Unable to open plugin archive.' => '플러그인 보관소 열기 비활성화', + 'There is no file in the plugin archive.' => '플러그인 보관소에 파일이 없습니다', + 'Create tasks in bulk' => '벌크에 할일 생성', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + 'There is no plugin available.' => '사용할 수 있는 플러그인이 없습니다', + 'Install' => '설치', + 'Update' => '업데이트', + 'Up to date' => '최신의', + 'Not available' => '사용 불가능', + 'Remove plugin' => '플러그인 삭제', + 'Do you really want to remove this plugin: "%s"?' => '플러그인을 삭제 하시겠습니까: "%s"?', + 'Uninstall' => '제거', + 'Listing' => '목록', + 'Metadata' => '메타 데이터', + 'Manage projects' => '프로젝트 관리', + 'Convert to task' => '할일로 변경', + 'Convert sub-task to task' => '서브 할일을 할일로 변경', + 'Do you really want to convert this sub-task to a task?' => '서브 할일을 일로 변경하시겠습니까?', + 'My task title' => '할일 제목', + // 'Enter one task by line.' => '', + 'Number of failed login:' => '로그인 실패 횟수', + 'Account locked until:' => '계정이 잠겼습니다:', + 'Email settings' => '이메일 설정', + 'Email sender address' => '이메일 송신자 주소', + 'Email transport' => '이메일 전송', + // 'Webhook token' => '', + 'Imports' => '가져오기', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/my_MY/translations.php b/sources/app/Locale/my_MY/translations.php index 36b3db0..94e9478 100644 --- a/sources/app/Locale/my_MY/translations.php +++ b/sources/app/Locale/my_MY/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Lihat tugas ini', 'Remove user' => 'Hapus pengguna', 'Do you really want to remove this user: "%s"?' => 'Anda yakin mahu menghapus pengguna ini : « %s » ?', - 'New user' => 'Pengguna baru', 'All users' => 'Semua pengguna', 'Username' => 'Nama pengguna', 'Password' => 'Kata laluan', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d tugas yang ditutup', 'No task for this project' => 'Tidak ada tugas dalam projek ini', 'Public link' => 'Pautan publik', - 'Change assignee' => 'Mengubah orang yand ditugaskan', - 'Change assignee for the task "%s"' => 'Mengubah orang yang ditugaskan untuk tugas « %s »', 'Timezone' => 'Zona waktu', 'Sorry, I didn\'t find this information in my database!' => 'Maaf, saya tidak menemukan informasi ini dalam basis data saya !', 'Page not found' => 'Halaman tidak ditemukan', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Tinggalkan deskripsi', 'Comment added successfully.' => 'Komentar berhasil ditambahkan.', 'Unable to create your comment.' => 'Tidak dapat menambahkan komentar anda.', - 'Edit this task' => 'Modifikasi tugas ini', 'Due Date' => 'Batas Tanggal Terakhir', 'Invalid date' => 'Tanggal tidak valid', 'Automatic actions' => 'Tindakan otomatis', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategori', 'Category:' => 'Kategori :', 'Categories' => 'Kategori', - 'Category not found.' => 'Kategori tidak ditemukan', 'Your category have been created successfully.' => 'Kategori anda berhasil dibuat.', 'Unable to create your category.' => 'Tidak dapat membuat kategori anda.', 'Your category have been updated successfully.' => 'Kategori anda berhasil diperbaharui.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Apakah anda yakin akan menghapus berkas ini « %s » ?', 'Attachments' => 'Lampiran', 'Edit the task' => 'Modifikasi tugas', - 'Edit the description' => 'Modifikasi deskripsi', 'Add a comment' => 'Tambahkan komentar', 'Edit a comment' => 'Modifikasi komentar', 'Summary' => 'Ringkasan', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Tidak ada otentifikasi eksternal yang aktif.', 'Password modified successfully.' => 'Kata laluan telah berjaya ditukar.', 'Unable to change the password.' => 'Tidak dapat merubah kata laluanr.', - 'Change category for the task "%s"' => 'Rubah kategori untuk tugas « %s »', 'Change category' => 'Tukar kategori', '%s updated the task %s' => '%s memperbaharui tugas %s', '%s opened the task %s' => '%s membuka tugas %s', @@ -413,9 +406,7 @@ return array( 'About' => 'Tentang', 'Database driver:' => 'Driver pengkalan data:', 'Board settings' => 'Pengaturan papan', - 'URL and token' => 'URL dan token', 'Webhook settings' => 'Penetapan webhook', - 'URL for task creation:' => 'URL untuk cipta tugas:', 'Reset token' => 'Menetap semula token', 'API endpoint:' => 'API endpoint :', 'Refresh interval for private board' => 'Interval pembaruan untuk papan pribadi', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Padam swimlane', 'Show default swimlane' => 'Tampilkan piawai swimlane', 'Swimlane modification for the project "%s"' => 'Modifikasi swimlane untuk projek « %s »', - 'Swimlane not found.' => 'Swimlane tidak ditemui.', 'Swimlane removed successfully.' => 'Swimlane telah dipadamkan.', 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane telah dikemaskini.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Semua swimlane', 'All colors' => 'Semua warna', 'Moved to column %s' => 'Pindah ke kolom %s', - 'Change description' => 'Ubah keterangan', 'User dashboard' => 'Papan Kenyataan pengguna', 'Allow only one subtask in progress at the same time for a user' => 'Izinkan hanya satu subtugas dalam proses secara bersamaan untuk satu pengguna', 'Edit column "%s"' => 'Modifikasi kolom « %s »', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'lihat papan di Kanboard', 'The task have been moved to the first swimlane' => 'Tugas telah dipindahkan ke swimlane pertama', 'The task have been moved to another swimlane:' => 'Tugas telah dipindahkan ke swimlane lain:', - 'Overdue tasks for the project "%s"' => 'Tugas terlambat untuk projek « %s »', 'New title: %s' => 'Judul baru : %s', 'The task is not assigned anymore' => 'Tugas tidak ditugaskan lagi', 'New assignee: %s' => 'Penerima baru : %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Hentikan timer', 'Start timer' => 'Mulai timer', 'Add project member' => 'Tambahkan anggota projek', - 'Enable notifications' => 'Aktifkan pemberitahuan', 'My activity stream' => 'Aliran kegiatan saya', 'My calendar' => 'Kalender saya', 'Search tasks' => 'Cari tugas', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + 'Overdue tasks for the project(s) "%s"' => 'Tugas terlambat untuk projek « %s »', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/nb_NO/translations.php b/sources/app/Locale/nb_NO/translations.php index 465efb5..cfd7ba1 100644 --- a/sources/app/Locale/nb_NO/translations.php +++ b/sources/app/Locale/nb_NO/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Se denne oppgaven', 'Remove user' => 'Fjern bruker', 'Do you really want to remove this user: "%s"?' => 'Vil du fjerne denne brukeren: "%s"?', - 'New user' => 'Ny bruker', 'All users' => 'Alle brukere', 'Username' => 'Brukernavn', 'Password' => 'Passord', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d lukkede oppgaver', 'No task for this project' => 'Ingen oppgaver i dette prosjektet', 'Public link' => 'Offentligt lenke', - 'Change assignee' => 'Tildel oppgaven til andre', - 'Change assignee for the task "%s"' => 'Endre tildeling av oppgaven: "%s"', 'Timezone' => 'Tidssone', 'Sorry, I didn\'t find this information in my database!' => 'Denne informasjonen kunne ikke finnes i databasen!', 'Page not found' => 'Siden er ikke funnet', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Legg inn en beskrivelse...', 'Comment added successfully.' => 'Kommentaren er lagt til.', 'Unable to create your comment.' => 'Din kommentar kunne ikke opprettes.', - 'Edit this task' => 'Rediger oppgaven', 'Due Date' => 'Forfallsdato', 'Invalid date' => 'Ugyldig dato', 'Automatic actions' => 'Automatiske handlinger', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategori', 'Category:' => 'Kategori:', 'Categories' => 'Kategorier', - 'Category not found.' => 'Kategori ikke funnet.', 'Your category have been created successfully.' => 'Kategorien er opprettet.', 'Unable to create your category.' => 'Kategorien kunne ikke opprettes.', 'Your category have been updated successfully.' => 'Kategorien er oppdatert.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Vil du fjerne filen: "%s"?', 'Attachments' => 'Vedlegg', 'Edit the task' => 'Rediger oppgaven', - 'Edit the description' => 'Rediger beskrivelsen', 'Add a comment' => 'Legg til en kommentar', 'Edit a comment' => 'Rediger en kommentar', 'Summary' => 'Sammendrag', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Ingen eksterne godkjenninger aktiveret.', 'Password modified successfully.' => 'Passord er endret.', 'Unable to change the password.' => 'Passordet kuenne ikke endres.', - 'Change category for the task "%s"' => 'Endre kategori for oppgaven "%s"', 'Change category' => 'Endre kategori', '%s updated the task %s' => '%s oppdaterte oppgaven %s', '%s opened the task %s' => '%s åpnet oppgaven %s', @@ -413,9 +406,7 @@ return array( 'About' => 'Om', 'Database driver:' => 'Database driver:', 'Board settings' => 'Innstillinger for prosjektside', - 'URL and token' => 'URL og token', 'Webhook settings' => 'Webhook innstillinger', - 'URL for task creation:' => 'URL for oppgaveopprettelse:', 'Reset token' => 'Resette token', 'API endpoint:' => 'API endpoint:', 'Refresh interval for private board' => 'Oppdateringsintervall for privat hovedside', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Fjern en svømmebane', 'Show default swimlane' => 'Vis standard svømmebane', // 'Swimlane modification for the project "%s"' => '', - 'Swimlane not found.' => 'Svømmebane ikke funnet', 'Swimlane removed successfully.' => 'Svømmebane fjernet', 'Swimlanes' => 'Svømmebaner', 'Swimlane updated successfully.' => 'Svømmebane oppdatert', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Alle svømmebaner', 'All colors' => 'Alle farger', // 'Moved to column %s' => '', - 'Change description' => 'Endre beskrivelse', 'User dashboard' => 'Brukerens hovedside', // 'Allow only one subtask in progress at the same time for a user' => '', // 'Edit column "%s"' => '', @@ -709,7 +698,6 @@ return array( // 'view the board on Kanboard' => '', // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', // 'New title: %s' => '', // 'The task is not assigned anymore' => '', // 'New assignee: %s' => '', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Stopp timer', 'Start timer' => 'Start timer', 'Add project member' => 'Legg til prosjektmedlem', - 'Enable notifications' => 'Aktiver varslinger', 'My activity stream' => 'Aktivitetslogg', 'My calendar' => 'Min kalender', 'Search tasks' => 'Søk oppgave', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/nl_NL/translations.php b/sources/app/Locale/nl_NL/translations.php index 3c3fa1e..69f2b86 100644 --- a/sources/app/Locale/nl_NL/translations.php +++ b/sources/app/Locale/nl_NL/translations.php @@ -35,7 +35,6 @@ return array( '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', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d gesloten taken', 'No task for this project' => 'Geen taken voor dit project', 'Public link' => 'Publieke link', - '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', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Schrijf een omschrijving', 'Comment added successfully.' => 'Commentaar succesvol toegevoegd.', 'Unable to create your comment.' => 'Commentaar toevoegen niet gelukt.', - 'Edit this task' => 'Deze taak aanpassen', 'Due Date' => 'Vervaldag', 'Invalid date' => 'Ongeldige datum', 'Automatic actions' => 'Geautomatiseerd acties', @@ -250,7 +246,6 @@ return array( '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.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Weet u zeker dat u dit bestand wil verwijderen: « %s » ?', 'Attachments' => 'Bijlages', 'Edit the task' => 'Taak aanpassen', - 'Edit the description' => 'Omschrijving aanpassen', 'Add a comment' => 'Commentaar toevoegen', 'Edit a comment' => 'Commentaar aanpassen', 'Summary' => 'Samenvatting', @@ -370,7 +364,6 @@ return array( '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', @@ -413,9 +406,7 @@ return array( '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', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Verwijder swinlane', '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.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Alle swimlanes', 'All colors' => 'Alle kleuren', 'Moved to column %s' => 'Verplaatst naar kolom %s', - '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', @@ -709,7 +698,6 @@ return array( // 'view the board on Kanboard' => '', // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', 'New title: %s' => 'Nieuw titel: %s', // 'The task is not assigned anymore' => '', // 'New assignee: %s' => '', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Stop timer', 'Start timer' => 'Start timer', 'Add project member' => 'Voeg projectlid toe', - 'Enable notifications' => 'Schakel notificaties in', 'My activity stream' => 'Mijn activiteiten', 'My calendar' => 'Mijn kalender', 'Search tasks' => 'Zoek taken', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/pl_PL/translations.php b/sources/app/Locale/pl_PL/translations.php index d06e347..7c132bb 100644 --- a/sources/app/Locale/pl_PL/translations.php +++ b/sources/app/Locale/pl_PL/translations.php @@ -35,7 +35,6 @@ return array( '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', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d zamkniętych zadań', 'No task for this project' => 'Brak zadań dla tego projektu', 'Public link' => 'Link publiczny', - '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', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Dodaj opis', 'Comment added successfully.' => 'Komentarz dodany', 'Unable to create your comment.' => 'Nie udało się dodać komentarza', - 'Edit this task' => 'Edytuj zadanie', 'Due Date' => 'Termin', 'Invalid date' => 'Błędna data', 'Automatic actions' => 'Akcje automatyczne', @@ -192,7 +188,7 @@ return array( 'Do you really want to remove this action: "%s"?' => 'Na pewno chcesz usunąć akcję "%s"?', 'Remove an automatic action' => 'Usuń akcję automatyczną', '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ę', + 'Assign the task to the person who does the action' => 'Przypisz zadanie do 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', 'Task modification' => 'Modyfikacja zadania', @@ -216,7 +212,7 @@ return array( 'Unknown' => 'Nieznany', 'Last logins' => 'Ostatnie logowania', 'Login date' => 'Data logowania', - 'Authentication method' => 'Sposób autentykacji', + 'Authentication method' => 'Sposób uwierzytelnienia', 'IP address' => 'Adres IP', 'User agent' => 'Przeglądarka', 'Persistent connections' => 'Stałe połączenia', @@ -235,10 +231,10 @@ return array( '%d comments' => '%d Komentarzy', '%d comment' => '%d Komentarz', 'Email address invalid' => 'Błędny adres email', - // 'Your external account is not linked anymore to your profile.' => '', - // 'Unable to unlink your external account.' => '', - // 'External authentication failed' => '', - // 'Your external account is linked to your profile successfully.' => '', + 'Your external account is not linked anymore to your profile.' => 'Twoje zewnętrzne konto nie jest już połączone z profilem', + 'Unable to unlink your external account.' => 'Nie można odłączyć zewnętrznego konta', + 'External authentication failed' => 'Uwierzytelnianie zewnętrzne zakończyło się niepowodzeniem', + 'Your external account is linked to your profile successfully.' => 'Twoje zewnętrzne konto zostało pomyślnie połączone z profilem', 'Email' => 'Email', 'Task removed successfully.' => 'Zadanie usunięto pomyślnie.', 'Unable to remove this task.' => 'Nie można usunąć tego zadania.', @@ -250,7 +246,6 @@ return array( '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ę', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Czy na pewno chcesz usunąć plik: "%s"?', 'Attachments' => 'Załączniki', 'Edit the task' => 'Edytuj zadanie', - 'Edit the description' => 'Edytuj opis', 'Add a comment' => 'Dodaj komentarz', 'Edit a comment' => 'Edytuj komentarz', 'Summary' => 'Podsumowanie', @@ -284,21 +278,21 @@ return array( 'hours' => 'godzin(y)', 'spent' => 'przeznaczono', 'estimated' => 'szacowany', - 'Sub-Tasks' => 'Podzadania', - 'Add a sub-task' => 'Dodaj podzadanie', + 'Sub-Tasks' => 'Pod-zadania', + 'Add a sub-task' => 'Dodaj pod-zadanie', 'Original estimate' => 'Szacowanie początkowe', 'Create another sub-task' => 'Dodaj kolejne pod-zadanie', - 'Time spent' => 'Przeznaczony czas', + 'Time spent' => 'Spędzony 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 podzadania.', + '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 zaktualizować tego podzadania.', - 'Unable to create your sub-task.' => 'Nie można utworzyć tego podzadania.', + 'Unable to update your sub-task.' => 'Nie można zaktualizować tego pod-zadania.', + 'Unable to create your sub-task.' => 'Nie można utworzyć tego pod-zadania.', 'Sub-task added successfully.' => 'Pod-zadanie utworzone pomyślnie', 'Maximum size: ' => 'Maksymalny rozmiar: ', 'Unable to upload the file.' => 'Nie można wczytać pliku.', @@ -343,9 +337,9 @@ return array( 'Disable public access' => 'Zablokuj dostęp publiczny', 'Enable public access' => 'Odblokuj dostęp publiczny', 'Public access disabled' => 'Dostęp publiczny zablokowany', - 'Do you really want to disable this project: "%s"?' => 'Czy na pewno chcesz zablokować projekt: "%s"?', - 'Do you really want to enable this project: "%s"?' => 'Czy na pewno chcesz odblokować projekt: "%s"?', - 'Project activation' => 'Aktywacja projekt', + 'Do you really want to disable this project: "%s"?' => 'Czy na pewno chcesz wyłączyć projekt: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Czy na pewno chcesz włączyć projekt: "%s"?', + 'Project activation' => 'Aktywacja projektu', '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 na pewno chcesz zduplikować to zadanie?', @@ -365,12 +359,11 @@ return array( 'Edit profile' => 'Edytuj profil', 'Change password' => 'Zmień hasło', 'Password modification' => 'Zmiana hasła', - 'External authentications' => 'Autentykacja zewnętrzna', + 'External authentications' => 'Uwierzytelnienia zewnętrzne', 'Never connected.' => 'Nigdy nie połączone.', - 'No external authentication enabled.' => 'Brak autentykacji zewnętrznych.', + 'No external authentication enabled.' => 'Brak włączonych uwierzytelnień 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', @@ -378,8 +371,8 @@ return array( '%s moved the task %s to the column "%s"' => '%s przeniósł zadanie %s do kolumny "%s"', '%s created the task %s' => '%s utworzył zadanie %s', '%s closed the task %s' => '%s zamknął zadanie %s', - '%s created a subtask for the task %s' => '%s utworzył podzadanie dla zadania %s', - '%s updated a subtask for the task %s' => '%s zaktualizował podzadanie dla zadania %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', @@ -388,8 +381,8 @@ return array( 'RSS feed' => 'Kanał RSS', '%s updated a comment on the task #%d' => '%s zaktualizował komentarz do zadania #%d', '%s commented on the task #%d' => '%s skomentował zadanie #%d', - '%s updated a subtask for the task #%d' => '%s zaktualizował podzadanie dla zadania #%d', - '%s created a subtask for the task #%d' => '%s utworzył podzadanie dla zadania #%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', @@ -413,11 +406,9 @@ return array( '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ń', + 'Webhook settings' => 'Ustawienia webhook', 'Reset token' => 'Resetuj token', - // 'API endpoint:' => '', + 'API endpoint:' => 'Endpoint API', 'Refresh interval for private board' => 'Częstotliwość odświeżania dla tablicy prywatnej', 'Refresh interval for public board' => 'Częstotliwość odświeżania dla tablicy publicznej', 'Task highlight period' => 'Okres wyróżniania zadań', @@ -440,7 +431,7 @@ return array( 'Confirmation' => 'Potwierdzenie', 'Allow everybody to access to this project' => 'Udostępnij ten projekt wszystkim', 'Everybody have access to this project.' => 'Wszyscy mają dostęp do tego projektu.', - // 'Webhooks' => '', + 'Webhooks' => 'Webhooki', // 'API' => '', 'Create a comment from an external provider' => 'Utwórz komentarz od zewnętrznego dostawcy', 'Project management' => 'Menadżer projektu', @@ -454,8 +445,8 @@ return array( 'Reportings' => 'Raporty', 'Task repartition for "%s"' => 'Przydział zadań dla "%s"', 'Analytics' => 'Analizy', - 'Subtask' => 'Podzadanie', - 'My subtasks' => 'Moje podzadania', + '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', @@ -466,7 +457,7 @@ return array( '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 podzadania musi być liczbą całkowitą', + '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ą', @@ -480,31 +471,30 @@ return array( 'Daily project summary export for "%s"' => 'Wygeneruj dzienny raport dla projektu: "%s"', 'Exports' => 'Eksporty', 'This export contains the number of tasks per column grouped per day.' => 'Ten eksport zawiera ilość zadań zgrupowanych w kolumnach na dzień', - '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', - 'Remove a swimlane' => 'Usuń proces', - '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 pomyślnie.', - 'Swimlanes' => 'Procesy', - 'Swimlane updated successfully.' => 'Proces zaktualizowany pomyślnie.', - 'The default swimlane have been updated successfully.' => 'Domyślny proces zaktualizowany pomyślnie.', - '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.', + 'Active swimlanes' => 'Aktywne tory', + 'Add a new swimlane' => 'Dodaj tor', + 'Change default swimlane' => 'Zmień domyślny tor', + 'Default swimlane' => 'Domyślny tor', + 'Do you really want to remove this swimlane: "%s"?' => 'Czy na pewno chcesz usunąć tor: "%s"?', + 'Inactive swimlanes' => 'Nieaktywne tory', + 'Remove a swimlane' => 'Usuń tor', + 'Show default swimlane' => 'Pokaż domyślny tor', + 'Swimlane modification for the project "%s"' => 'Edycja torów dla projektu "%s"', + 'Swimlane removed successfully.' => 'Tor usunięty pomyślnie.', + 'Swimlanes' => 'Tory', + 'Swimlane updated successfully.' => 'Zaktualizowano tor.', + 'The default swimlane have been updated successfully.' => 'Domyślny tor zaktualizowany pomyślnie.', + 'Unable to remove this swimlane.' => 'Nie można usunąć toru.', + 'Unable to update this swimlane.' => 'Nie można zaktualizować toru.', + 'Your swimlane have been created successfully.' => 'Tor utworzony pomyślnie.', 'Example: "Bug, Feature Request, Improvement"' => 'Przykład: "Błąd, Żądanie Funkcjonalności, Udoskonalenia"', 'Default categories for new projects (Comma-separated)' => 'Domyślne kategorie dla nowych projektów (oddzielone przecinkiem)', 'Integrations' => 'Integracje', 'Integration with third-party services' => 'Integracja z usługami firm trzecich', 'Subtask Id' => 'ID pod-zadania', - 'Subtasks' => 'Podzadania', - 'Subtasks Export' => 'Eksport podzadań', - 'Subtasks exportation for "%s"' => 'Wygeneruj raport podzadań dla projektu "%s"', + 'Subtasks' => 'Pod-zadania', + 'Subtasks Export' => 'Eksport pod-zadań', + 'Subtasks exportation for "%s"' => 'Wygeneruj raport pod-zadań dla projektu "%s"', 'Task Title' => 'Nazwa zadania', 'Untitled' => 'Bez nazwy', 'Application default' => 'Domyślne dla aplikacji', @@ -514,10 +504,9 @@ return array( 'Calendar' => 'Kalendarz', 'Next' => 'Następny', '#%d' => 'nr %d', - 'All swimlanes' => 'Wszystkie procesy', + 'All swimlanes' => 'Wszystkie tory', 'All colors' => 'Wszystkie kolory', 'Moved to column %s' => 'Przeniosiono do kolumny %s', - 'Change description' => 'Zmień opis', 'User dashboard' => 'Panel użytkownika', 'Allow only one subtask in progress at the same time for a user' => 'Zezwalaj na tylko jedno pod-zadanie o statusie "w trakcie" jednocześnie', 'Edit column "%s"' => 'Zmień kolumnę "%s"', @@ -611,15 +600,15 @@ return array( 'Reference currency' => 'Waluta referencyjna', 'The currency rate have been added successfully.' => 'Dodano kurs waluty', 'Unable to add this currency rate.' => 'Nie można dodać kursu waluty', - // 'Webhook URL' => '', + 'Webhook URL' => 'Adres webhooka', '%s remove the assignee of the task %s' => '%s usunął osobę przypisaną do zadania %s', - // 'Enable Gravatar images' => '', + 'Enable Gravatar images' => 'Włącz Gravatar', 'Information' => 'Informacje', 'Check two factor authentication code' => 'Sprawdź kod weryfikujący', 'The two factor authentication code is not valid.' => 'Kod weryfikujący niepoprawny', 'The two factor authentication code is valid.' => 'Kod weryfikujący poprawny', 'Code' => 'Kod', - 'Two factor authentication' => 'Uwierzytelnianie dwustopniowe', + 'Two factor authentication' => 'Dwustopniowe uwierzytelnianie', 'This QR code contains the key URI: ' => 'Ten kod QR zawiera URI klucza: ', 'Check my code' => 'Sprawdź kod', 'Secret key: ' => 'Tajny kod: ', @@ -628,15 +617,15 @@ return array( '%s via Kanboard' => '%s poprzez Kanboard', 'Burndown chart for "%s"' => 'Wykres Burndown dla "%s"', 'Burndown chart' => 'Wykres Burndown', - // 'This chart show the task complexity over the time (Work Remaining).' => '', + 'This chart show the task complexity over the time (Work Remaining).' => 'Ten wykres pokazuje złożoność zadania na przestrzeni czasu (pozostała praca).', 'Screenshot taken %s' => 'Zrzut ekranu zapisany %s', 'Add a screenshot' => 'Dołącz zrzut ekranu', 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Zrób zrzut ekranu i wciśnij CTRL+V by dodać go tutaj.', 'Screenshot uploaded successfully.' => 'Zrzut ekranu dodany.', 'SEK - Swedish Krona' => 'SEK - Korona szwedzka', 'Identifier' => 'Identyfikator', - 'Disable two factor authentication' => 'Wyłącz uwierzytelnianie dwuetapowe', - 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Czy na pewno chcesz wyłączyć uwierzytelnianie dwuetapowe dla tego użytkownika: "%s"?', + 'Disable two factor authentication' => 'Wyłącz dwustopniowe uwierzytelnianie', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Czy na pewno chcesz wyłączyć dwustopniowe uwierzytelnianie dla tego użytkownika: "%s"?', 'Edit link' => 'Edytuj link', 'Start to type task title...' => 'Rozpocznij wpisywanie tytułu zadania...', 'A task cannot be linked to itself' => 'Link do zadania nie może wskazywać na samego siebie', @@ -646,7 +635,7 @@ return array( 'The identifier must be unique' => 'Identyfikator musi być unikatowy', 'This linked task id doesn\'t exists' => 'Id zadania nie istnieje', 'This value must be alphanumeric' => 'Ta wartość musi być alfanumeryczna', - 'Edit recurrence' => 'Rekurencja', + 'Edit recurrence' => 'Edytuj rekurencje', 'Generate recurrent task' => 'Włącz rekurencje', 'Trigger to generate recurrent task' => 'Wyzwalacz tworzący zadanie cykliczne', 'Factor to calculate new due date' => 'Czynnik wyliczający nowy termin', @@ -671,7 +660,7 @@ return array( 'Calendar settings' => 'Ustawienia kalendarza', 'Project calendar view' => 'Widok kalendarza projektu', 'Project settings' => 'Ustawienia Projektu', - 'Show subtasks based on the time tracking' => 'Pokaż podzadania w śledzeniu czasu', + 'Show subtasks based on the time tracking' => 'Pokaż pod-zadania w śledzeniu czasu', 'Show tasks based on the creation date' => 'Pokaż zadania względem daty utworzenia', 'Show tasks based on the start date' => 'Pokaż zadania względem daty rozpoczęcia', 'Subtasks time tracking' => 'Śledzenie czasu pod-zadań', @@ -680,65 +669,63 @@ return array( // 'iCal feed' => '', 'Preferences' => 'Ustawienia', 'Security' => 'Zabezpieczenia', - 'Two factor authentication disabled' => 'Uwierzytelnianie dwuetapowe wyłączone', - 'Two factor authentication enabled' => 'Uwierzytelnianie dwuetapowe włączone', + 'Two factor authentication disabled' => 'Dwustopniowe uwierzytelnianie wyłączone', + 'Two factor authentication enabled' => 'Dwustopniowe uwierzytelnianie włączone', 'Unable to update this user.' => 'Nie można zaktualizować tego użytkownika', 'There is no user management for private projects.' => 'Projekty prywatne nie wspierają zarządzania użytkownikami. Projekt prywatny ma tylko jednego użytkownika.', - // 'User that will receive the email' => '', - // 'Email subject' => '', + 'User that will receive the email' => 'Adresat', + 'Email subject' => 'Temat', 'Date' => 'Data', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - 'Send a task by email to someone' => 'Wyślij zadanie mailem do kogokolwiek', + 'Add a comment log when moving the task between columns' => 'Wygeneruj komentarz pod zadaniem podczas przenoszenia między kolumnami', + 'Move the task to another column when the category is changed' => 'Przenieś zadanie do innej kolumny gdy kategoria ulegnie zmianie', + 'Send a task by email to someone' => 'Wyślij zadanie emailem do kogoś', 'Reopen a task' => 'Otwórz ponownie zadanie', 'Column change' => 'Zmiana kolumny', 'Position change' => 'Zmiana pozycji', - 'Swimlane change' => 'Zmiana Swimlane', + 'Swimlane change' => 'Zmiana toru', 'Assignee change' => 'Zmiana przypisanego użytkownika', - // '[%s] Overdue tasks' => '', + '[%s] Overdue tasks' => '[%s] zaległych zadań', 'Notification' => 'Powiadomienie', - // '%s moved the task #%d to the first swimlane' => '', - // '%s moved the task #%d to the swimlane "%s"' => '', - 'Swimlane' => 'Proces', + '%s moved the task #%d to the first swimlane' => '%s przeniosł zadanie #%d na pierwszy tor', + '%s moved the task #%d to the swimlane "%s"' => '%s przeniosł zadanie #%d na tor "%s"', + 'Swimlane' => 'Tor', // 'Gravatar' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', - // 'This report contains all subtasks information for the given date range.' => '', - // 'This report contains all tasks information for the given date range.' => '', - // 'Project activities for %s' => '', - // 'view the board on Kanboard' => '', - // 'The task have been moved to the first swimlane' => '', - // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', + '%s moved the task %s to the first swimlane' => '%s przeniosł zadanie %s na pierwszy tor', + '%s moved the task %s to the swimlane "%s"' => '%s przeniosł zadanie %s na tor "%s"', + 'This report contains all subtasks information for the given date range.' => 'Niniejszy raport zawiera wszystkie informacje o pod-zadaniach dla podanego zakresu dat.', + 'This report contains all tasks information for the given date range.' => 'Niniejszy raport zawiera wszystkie informacje o zadaniach dla podanego zakresu dat.', + 'Project activities for %s' => 'Aktywności w ramach projektu dla %s', + 'view the board on Kanboard' => 'Zobacz tablice', + 'The task have been moved to the first swimlane' => 'Zadanie zostało przeniesione do piewszego toru', + 'The task have been moved to another swimlane:' => 'Zadanie zostało przeniesione do innego toru:', 'New title: %s' => 'Nowy tytuł: %s', 'The task is not assigned anymore' => 'Brak osoby odpowiedzialnej za zadanie', 'New assignee: %s' => 'Nowy odpowiedzialny: %s', 'There is no category now' => 'Aktualnie zadanie nie posiada kategorii', 'New category: %s' => 'Nowa kategoria: %s', 'New color: %s' => 'Nowy kolor: %s', - // 'New complexity: %d' => '', - // 'The due date have been removed' => '', - // 'There is no description anymore' => '', - // 'Recurrence settings have been modified' => '', - // 'Time spent changed: %sh' => '', - // 'Time estimated changed: %sh' => '', - // 'The field "%s" have been updated' => '', - // 'The description has been modified:' => '', - // 'Do you really want to close the task "%s" as well as all subtasks?' => '', + 'New complexity: %d' => 'Nowa złożoność: %d', + 'The due date have been removed' => 'Termin został usunięty', + 'There is no description anymore' => 'Nie ma już opisu', + 'Recurrence settings have been modified' => 'Ustawienia cyklu zostały zmienione', + 'Time spent changed: %sh' => 'Spędzony czas uległ zmianie: %sh', + 'Time estimated changed: %sh' => 'Szacowany czas uległ zmianie: %sh', + 'The field "%s" have been updated' => 'Pole "%s" zostało zaktualizowane', + 'The description has been modified:' => 'Opis został zmodyfikowany:', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Naprawdę chcesz zamknąć zadanie "%s" wraz z wszystkimi pod-zadaniami?', 'I want to receive notifications for:' => 'Wysyłaj powiadomienia dla:', 'All tasks' => 'Wszystkich zadań', 'Only for tasks assigned to me' => 'Tylko zadań przypisanych do mnie', 'Only for tasks created by me' => 'Tylko zadań utworzonych przeze mnie', 'Only for tasks created by me and assigned to me' => 'Tylko zadań przypisanych lub utworzonych przeze mnie', // '%%Y-%%m-%%d' => '', - // 'Total for all columns' => '', - // 'You need at least 2 days of data to show the chart.' => '', + 'Total for all columns' => 'Ogółem dla wszystkich kolumn', + 'You need at least 2 days of data to show the chart.' => 'Potrzebujesz przynajmniej 2 dni by wyświetlić wykres', // '<15m' => '', // '<30m' => '', 'Stop timer' => 'Zatrzymaj pomiar czasu', 'Start timer' => 'Uruchom pomiar czasu', - 'Add project member' => 'Dodaj członka projektu', - 'Enable notifications' => 'Włącz powiadomienia', + 'Add project member' => 'Dodaj uczestnika projektu', 'My activity stream' => 'Moja aktywność', 'My calendar' => 'Mój kalendarz', 'Search tasks' => 'Szukaj zadań', @@ -771,42 +758,42 @@ return array( 'Search by category: ' => 'Szukaj wg kategorii:', 'Search by description: ' => 'Szukaj wg opisu:', 'Search by due date: ' => 'Szukaj wg terminu:', - // 'Lead and Cycle time for "%s"' => '', - // 'Average time spent into each column for "%s"' => '', - // 'Average time spent into each column' => '', - // 'Average time spent' => '', - // 'This chart show the average time spent into each column for the last %d tasks.' => '', - // 'Average Lead and Cycle time' => '', - // 'Average lead time: ' => '', - // 'Average cycle time: ' => '', - // 'Cycle Time' => '', - // 'Lead Time' => '', - // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', - // 'Average time into each column' => '', - // 'Lead and cycle time' => '', - // 'Lead time: ' => '', - // 'Cycle time: ' => '', + 'Lead and Cycle time for "%s"' => 'Czas cyklu i realizacji dla "%s"', + 'Average time spent into each column for "%s"' => 'Średni czas spędzony w każdej z kolumn dla "%s"', + 'Average time spent into each column' => 'Średni czas spędzony w każdej z kolumn', + 'Average time spent' => 'Średni spędzony czas', + 'This chart show the average time spent into each column for the last %d tasks.' => 'Niniejszy wykres pokazuje średni czas spędzony w każdej z kolumn dla ostatnich %d zadań.', + 'Average Lead and Cycle time' => 'Średni czas cyklu i realizacji', + 'Average lead time: ' => 'Średni czas realizacji:', + 'Average cycle time: ' => 'Średni czas cyklu:', + 'Cycle Time' => 'Czas cyklu', + 'Lead Time' => 'Czas realizacji', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Niniejszy wykres pokazuje średni czas cyklu i realizacji dla ostatnich %d zadań na przestrzeni czasu.', + 'Average time into each column' => 'Średni czas dla każdej kolumny', + 'Lead and cycle time' => 'Czas cyklu i realizacji', + 'Lead time: ' => 'Czas realizacji:', + 'Cycle time: ' => 'Czas cyklu:', 'Time spent into each column' => 'Czas spędzony przez zadanie w każdej z kolumn', - // 'The lead time is the duration between the task creation and the completion.' => '', - // 'The cycle time is the duration between the start date and the completion.' => '', - // 'If the task is not closed the current time is used instead of the completion date.' => '', - // 'Set automatically the start date' => '', - 'Edit Authentication' => 'Edycja autoryzacji', - // 'Remote user' => '', - // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', - // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', + 'The lead time is the duration between the task creation and the completion.' => 'Czas realizacji pomiędzy utworzeniem a ukończeniem zadania.', + 'The cycle time is the duration between the start date and the completion.' => 'Czas cyklu pomiędzy datą rozpoczęcia a ukończeniem zadania.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Jeśli zadanie nie jest zamknięte, bieżący czas zostaje użyty zamiast daty ukończenia.', + 'Set automatically the start date' => 'Ustaw automatycznie datę rozpoczęcia', + 'Edit Authentication' => 'Edycja uwierzytelnienia', + 'Remote user' => 'Zdalny użytkownik', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Zdalni użykownicy nie przechowują swojego hasła w bazie danych Kanboard, przykłady: konta LDAP, Google and Github.', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Jeśli zaznaczysz "Zablokuj możliwość logowania", dane podane przy logowaniu zostaną zignorowane.', 'New remote user' => 'Nowy użytkownik zdalny', 'New local user' => 'Nowy użytkownik lokalny', 'Default task color' => 'Domyślny kolor zadań', - 'This feature does not work with all browsers.' => 'Ta funkcja może nie działać z każdą przeglądarką', - // 'There is no destination project available.' => '', - // 'Trigger automatically subtask time tracking' => '', - // 'Include closed tasks in the cumulative flow diagram' => '', - 'Current swimlane: %s' => 'Bieżący swimlane: %s', + 'This feature does not work with all browsers.' => 'Ta funkcja może nie działać z każdą przeglądarką.', + 'There is no destination project available.' => 'Żaden docelowy projekt nie jest aktualnie dostępny.', + 'Trigger automatically subtask time tracking' => 'Ustaw automatyczne śledzenie czasu dla pod-zadań', + 'Include closed tasks in the cumulative flow diagram' => 'Obejmuj zamknięte zadania w zbiorczym diagramie przepływu', + 'Current swimlane: %s' => 'Bieżący tor: %s', 'Current column: %s' => 'Bieżąca kolumna: %s', 'Current category: %s' => 'Bieżąca kategoria: %s', 'no category' => 'brak kategorii', - // 'Current assignee: %s' => '', + 'Current assignee: %s' => 'Aktualnie odpowiedzialna osoba: %s', 'not assigned' => 'Brak osoby odpowiedzialnej', 'Author:' => 'Autor', 'contributors' => 'współautorzy', @@ -820,18 +807,18 @@ return array( 'Start date:' => 'Data rozpoczęcia:', 'Due date:' => 'Termin', 'There is no start date or due date for this task.' => 'Brak daty rozpoczęcia lub terminu zadania', - // 'Moving or resizing a task will change the start and due date of the task.' => '', - // 'There is no task in your project.' => '', + 'Moving or resizing a task will change the start and due date of the task.' => 'Przeniesienie bądź edycja zmieni datę rozpoczęcia oraz termin ukończenia zadania.', + 'There is no task in your project.' => 'Brak zadań w projekcie.', 'Gantt chart' => 'Wykres Gantta', 'People who are project managers' => 'Użytkownicy będący menedżerami projektu', 'People who are project members' => 'Użytkownicy będący uczestnikami projektu', - // 'NOK - Norwegian Krone' => '', + 'NOK - Norwegian Krone' => 'NOK - Korona norweska', 'Show this column' => 'Pokaż tą kolumnę', 'Hide this column' => 'Ukryj tą kolumnę', 'open file' => 'otwórz plik', 'End date' => 'Data zakończenia', 'Users overview' => 'Przegląd użytkowników', - 'Members' => 'Uczestnicy', + 'Members' => 'Członkowie', 'Shared project' => 'Projekt udostępniony', 'Project managers' => 'Menedżerowie projektu', 'Gantt chart for all projects' => 'Wykres Gantta dla wszystkich projektów', @@ -844,11 +831,11 @@ return array( 'Change task color when using a specific task link' => 'Zmień kolor zadania używając specjalnego adresu URL', 'Task link creation or modification' => 'Adres URL do utworzenia zadania lub modyfikacji', 'Milestone' => 'Kamień milowy', - // 'Documentation: %s' => '', - // 'Switch to the Gantt chart view' => '', - // 'Reset the search/filter box' => '', + 'Documentation: %s' => 'Dokumentacja: %s', + 'Switch to the Gantt chart view' => 'Przełącz na wykres Gantta', + 'Reset the search/filter box' => 'Zresetuj pole wyszukiwania/filtrowania', 'Documentation' => 'Dokumentacja', - // 'Table of contents' => '', + 'Table of contents' => 'Tablica zawartości', // 'Gantt' => '', 'Author' => 'Autor', 'Version' => 'Wersja', @@ -858,60 +845,60 @@ return array( 'Remove maximum column height' => 'Zwiń kolumny', 'My notifications' => 'Powiadomienia', 'Custom filters' => 'Dostosuj filtry', - // 'Your custom filter have been created successfully.' => '', - // 'Unable to create your custom filter.' => '', - // 'Custom filter removed successfully.' => '', - // 'Unable to remove this custom filter.' => '', - // 'Edit custom filter' => '', - // 'Your custom filter have been updated successfully.' => '', - // 'Unable to update custom filter.' => '', + 'Your custom filter have been created successfully.' => 'Niestandardowy filtr został utworzony.', + 'Unable to create your custom filter.' => 'Nie można utworzyć niestandardowego filtra.', + 'Custom filter removed successfully.' => 'Niestandardowy filtr został usunięty.', + 'Unable to remove this custom filter.' => 'Nie można usunąć niestandardowego filtra.', + 'Edit custom filter' => 'Edytuj niestandardowy filtr', + 'Your custom filter have been updated successfully.' => 'Niestandardowy filtr został zaktualizowany', + 'Unable to update custom filter.' => 'Nie można zaktualizować niestandardowego filtra.', // 'Web' => '', 'New attachment on task #%d: %s' => 'Nowy załącznik do zadania #%d: %s', 'New comment on task #%d' => 'Nowy komentarz do zadania #%d', 'Comment updated on task #%d' => 'Aktualizacja komentarza do zadania #%d', - 'New subtask on task #%d' => 'Nowe podzadanie dla zadania #%d', - 'Subtask updated on task #%d' => 'Aktualizacja podzadania w zadaniu #%d', + 'New subtask on task #%d' => 'Nowe pod-zadanie dla zadania #%d', + 'Subtask updated on task #%d' => 'Aktualizacja pod-zadania w zadaniu #%d', 'New task #%d: %s' => 'Nowe zadanie #%d: %s', 'Task updated #%d' => 'Aktualizacja zadania #%d', 'Task #%d closed' => 'Zamknięto zadanie #%d', 'Task #%d opened' => 'Otwarto zadanie #%d', 'Column changed for task #%d' => 'Zmieniono kolumnę zadania #%d', 'New position for task #%d' => 'Ustalono nową pozycję zadania #%d', - 'Swimlane changed for task #%d' => 'Zmieniono swimlane dla zadania #%d', + 'Swimlane changed for task #%d' => 'Zmieniono tor dla zadania #%d', 'Assignee changed on task #%d' => 'Zmieniono osobę odpowiedzialną dla zadania #%d', - // '%d overdue tasks' => '', - // 'Task #%d is overdue' => '', + '%d overdue tasks' => '%d zaległych zadań', + 'Task #%d is overdue' => 'Zadanie #%d jest zaległe', 'No new notifications.' => 'Brak nowych powiadomień.', 'Mark all as read' => 'Oznacz wszystkie jako przeczytane', 'Mark as read' => 'Oznacz jako przeczytane', - // 'Total number of tasks in this column across all swimlanes' => '', - 'Collapse swimlane' => 'Zwiń swimlane', - 'Expand swimlane' => 'Rozwiń swimlane', + 'Total number of tasks in this column across all swimlanes' => 'Całkowita liczba zadań z tej kolumny z wszystkich torów', + 'Collapse swimlane' => 'Zwiń tor', + 'Expand swimlane' => 'Rozwiń tor', 'Add a new filter' => 'Dodaj nowy filtr', 'Share with all project members' => 'Udostępnij wszystkim uczestnikom projektu', - // 'Shared' => '', + 'Shared' => 'Udostępnione', 'Owner' => 'Właściciel', 'Unread notifications' => 'Nieprzeczytane powiadomienia', 'Notification methods:' => 'Metody powiadomień:', - // 'Import tasks from CSV file' => '', - // 'Unable to read your file' => '', - // '%d task(s) have been imported successfully.' => '', - // 'Nothing have been imported!' => '', - // 'Import users from CSV file' => '', - // '%d user(s) have been imported successfully.' => '', + 'Import tasks from CSV file' => 'Importuj zadania z pliku CSV', + 'Unable to read your file' => 'Nie można odczytać pliku', + '%d task(s) have been imported successfully.' => '%d zadań zostało zaimportowanych.', + 'Nothing have been imported!' => 'Nic nie zostało zaimportowane!', + 'Import users from CSV file' => 'Importuj użytkowników z pliku CSV', + '%d user(s) have been imported successfully.' => '%d użytkowników zostało zaimportowanych.', 'Comma' => 'Przecinek', 'Semi-colon' => 'Średnik', 'Tab' => 'Tabulacja', 'Vertical bar' => 'Kreska pionowa', 'Double Quote' => 'Cudzysłów', 'Single Quote' => 'Apostrof', - // '%s attached a file to the task #%d' => '', - // 'There is no column or swimlane activated in your project!' => '', + '%s attached a file to the task #%d' => '%s dołączył(a) plik do zadania #%d', + 'There is no column or swimlane activated in your project!' => 'Żaden tor badź kolumna nie została aktywowana!', 'Append filter (instead of replacement)' => 'Dołączaj filtr do zastosowanego filtru(zamiast przełączać)', - // 'Append/Replace' => '', - // 'Append' => '', - // 'Replace' => '', - // 'Import' => '', + 'Append/Replace' => 'Dołącz/Zastąp', + 'Append' => 'Dołącz', + 'Replace' => 'Zastąp', + 'Import' => 'Importuj', 'change sorting' => 'odwróć sortowanie', 'Tasks Importation' => 'Import zadań', 'Delimiter' => 'Separator pola', @@ -924,16 +911,16 @@ return array( 'Duplicates are not verified for you' => 'Duplikaty nie będą weryfikowane', 'The due date must use the ISO format: YYYY-MM-DD' => 'Data musi być w formacie ISO: YYYY-MM-DD', 'Download CSV template' => 'Pobierz szablon pliku CSV', - // 'No external integration registered.' => '', + 'No external integration registered.' => 'Żadna zewnętrzna integracja nie została zarejestrowana.', 'Duplicates are not imported' => 'Duplikaty nie zostaną zaimportowane', - // 'Usernames must be lowercase and unique' => '', - // 'Passwords will be encrypted if present' => '', - // '%s attached a new file to the task %s' => '', + 'Usernames must be lowercase and unique' => 'Nazwy użytkowników muszą być unikalne i składać się z małych liter', + 'Passwords will be encrypted if present' => 'Hasła zostaną zaszyfrowane jeśli występują', + '%s attached a new file to the task %s' => '%s załączył nowy plik do zadania %s', 'Link type' => 'Rodzaj link\'u', - // 'Assign automatically a category based on a link' => '', - // 'BAM - Konvertible Mark' => '', - // 'Assignee Username' => '', - // 'Assignee Name' => '', + 'Assign automatically a category based on a link' => 'Przypisz kategorię automatycznie na podstawie linku', + 'BAM - Konvertible Mark' => 'BAM - Bośnia i Hercegowina Cabrio Marka', + 'Assignee Username' => 'Przypisz nazwę użytkownika', + 'Assignee Name' => 'Przypisz imię', 'Groups' => 'Grupy', 'Members of %s' => 'Członkowie %s', 'New group' => 'Nowa grupa', @@ -962,11 +949,11 @@ return array( 'View all groups' => 'Wyświetl wszystkie grupy', 'View group members' => 'Wyświetl wszystkich członków grupy', 'There is no user available.' => 'Żaden użytkownik nie jest dostępny.', - 'Do you really want to remove the user "%s" from the group "%s"?' => 'Czy napewno chcesz usunąć użytkownika "%s" z grupy "%s"?', + 'Do you really want to remove the user "%s" from the group "%s"?' => 'Czy na pewno chcesz usunąć użytkownika "%s" z grupy "%s"?', 'There is no group.' => 'Nie utworzono jeszcze żadnej grupy.', 'External Id' => 'Zewnętrzny Id', 'Add group member' => 'Dodaj członka grupy', - 'Do you really want to remove this group: "%s"?' => 'Czy napewno chcesz usunąć grupę "%s"?', + 'Do you really want to remove this group: "%s"?' => 'Czy na pewno chcesz usunąć grupę "%s"?', 'There is no user in this group.' => 'Wybrana grupa nie posiada członków.', 'Remove this user' => 'Usuń użytkownika', 'Permissions' => 'Prawa dostępu', @@ -981,53 +968,53 @@ return array( 'Enter group name...' => 'Wprowadź nazwę grupy...', 'Role:' => 'Rola:', 'Project members' => 'Uczestnicy projektu', - // 'Compare hours for "%s"' => '', + 'Compare hours for "%s"' => 'Porównaj godziny dla "%s"', '%s mentioned you in the task #%d' => '%s wspomiał o Tobie w zadaniu #%d', '%s mentioned you in a comment on the task #%d' => '%s wspomiał o Tobie w komentarzu do zadania #%d', 'You were mentioned in the task #%d' => 'Wspomiano o Tobie w zadaniu #%d', 'You were mentioned in a comment on the task #%d' => 'Wspomiano o Tobie w komentarzu do zadania #%d', 'Mentioned' => 'Wspomiano', - // 'Compare Estimated Time vs Actual Time' => '', - // 'Estimated hours: ' => '', - // 'Actual hours: ' => '', - // 'Hours Spent' => '', - // 'Hours Estimated' => '', - // 'Estimated Time' => '', - // 'Actual Time' => '', - // 'Estimated vs actual time' => '', - // 'RUB - Russian Ruble' => '', - // 'Assign the task to the person who does the action when the column is changed' => '', - // 'Close a task in a specific column' => '', + 'Compare Estimated Time vs Actual Time' => 'Porównaj szacowany czas z rzeczywistym', + 'Estimated hours: ' => 'Szacowane godziny: ', + 'Actual hours: ' => 'Rzeczywiste godziny: ', + 'Hours Spent' => 'Spędzone godziny', + 'Hours Estimated' => 'Szacowane godziny', + 'Estimated Time' => 'Szacowany czas', + 'Actual Time' => 'Rzeczywisty czas', + 'Estimated vs actual time' => 'Szacowany vs rzeczywisty czas', + 'RUB - Russian Ruble' => 'RUB - Rosyjskie Ruble', + 'Assign the task to the person who does the action when the column is changed' => 'Przypisz zadanie do osoby wykonującej akcję gdy kolumna zostanie zmodyfikowana', + 'Close a task in a specific column' => 'Zamknij zadanie w określonej kolumnie', // 'Time-based One-time Password Algorithm' => '', - // 'Two-Factor Provider: ' => '', - // 'Disable two-factor authentication' => '', - // 'Enable two-factor authentication' => '', - 'There is no integration registered at the moment.' => 'W chwili obecnej funkcjonalność ta została wyłączona.', + 'Two-Factor Provider: ' => 'Dostawca: ', + 'Disable two-factor authentication' => 'Wyłącz dwustopniowe uwierzytelnianie', + 'Enable two-factor authentication' => 'Włącz dwustopniowe uwierzytelnianie', + 'There is no integration registered at the moment.' => 'W chwili obecnej nie ma zarejestrowanej żadnej integracji.', 'Password Reset for Kanboard' => 'Resetuj hasło do Kanboarda', 'Forgot password?' => 'Nie pamiętasz hasła?', - 'Enable "Forget Password"' => 'Włącz możliwość odzyskiwania zapomianego hasła przez użytkowników', + 'Enable "Forget Password"' => 'Włącz "Nie pamiętasz hasła?"', 'Password Reset' => 'Resetuj hasło', 'New password' => 'Nowe hasło', 'Change Password' => 'Zmień hasło', 'To reset your password click on this link:' => 'Kliknij w poniższy link, aby zresetować hasło:', 'Last Password Reset' => 'Ostatnie odzyskiwanie hasła', 'The password has never been reinitialized.' => 'Hasło nigdy nie było odzyskiwane.', - // 'Creation' => '', - // 'Expiration' => '', - 'Password reset history' => 'Historia hasła', - // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '', - // 'Do you really want to close all tasks of this column?' => '', - // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '', + 'Creation' => 'Utworzenie', + 'Expiration' => 'Wygaśnięcie', + 'Password reset history' => 'Historia resetowania hasła', + 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => 'Wszystkie zadania z kolumny "%s" i toru "%s" zostały zamknięte.', + 'Do you really want to close all tasks of this column?' => 'Na pewno chcesz zamknąć wszystkie zadania z tej kolumny?', + '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '%d zadania z kolumny "%s" i toru "%s" zostaną zamknięte.', 'Close all tasks of this column' => 'Zamknij wszystkie zadania w tej kolumnie', 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Wtyczki obsługujące dodatkowe powiadomienia nie zostały zainstalowane. Dalej jednak możesz korzystać z standardowych powiadomień (sprawdź w ustawieniach Twojego profilu).', 'My dashboard' => 'Mój dashboard', 'My profile' => 'Mój profil', - // 'Project owner: ' => '', + 'Project owner: ' => 'Właściciel projektu: ', 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'Identyfikator projektu jest opcjonalny i musi być alfanumeryczny, przykład: MYPROJECT.', 'Project owner' => 'Właściciel projektu', 'Those dates are useful for the project Gantt chart.' => 'Daty te są przydatne dla wykresu Gantta.', 'Private projects do not have users and groups management.' => 'Projekty prywatne nie wspierają obsługi użytkowników i grup.', - // 'There is no project member.' => '', + 'There is no project member.' => 'Projekt nie ma uczestników.', 'Priority' => 'Priorytet', 'Task priority' => 'Priorytety zadań', 'General' => 'Ogólne', @@ -1036,15 +1023,15 @@ return array( 'Lowest priority' => 'Najniższy priorytet', 'Highest priority' => 'Najwyższy priorytet', 'If you put zero to the low and high priority, this feature will be disabled.' => 'Jeżeli dla najniższego i najwyższego priorytetu ustawisz 0, to priorytety dla tablicy zostaną wyłączone.', - // 'Close a task when there is no activity' => '', - // 'Duration in days' => '', - // 'Send email when there is no activity on a task' => '', - // 'Unable to fetch link information.' => '', + 'Close a task when there is no activity' => 'Zamknij zadanie gdy nie jest aktywne', + 'Duration in days' => 'Czas trwania w dniach', + 'Send email when there is no activity on a task' => 'Wyślij email gdy zadanie nie jest aktywne', + 'Unable to fetch link information.' => 'Nie można pobrać informacji o połączeniach.', // 'Daily background job for tasks' => '', 'Auto' => 'Automatyczny', 'Related' => 'Powiązanie', 'Attachment' => 'Załącznik', - // 'Title not found' => '', + 'Title not found' => 'Nie odnaleziono tytułu', 'Web Link' => 'Link URL', 'External links' => 'Linki zewnętrzne', 'Add external link' => 'Dodaj link zewnętrzny', @@ -1057,7 +1044,7 @@ return array( 'Copy and paste your link here...' => 'Skopiuj i wklej link tutaj ...', // 'URL' => '', 'Internal links' => 'Linki do innych zadań', - // 'Assign to me' => '', + 'Assign to me' => 'Przypisz do mnie', 'Me' => 'JA', 'Do not duplicate anything' => 'Nie kopiuj żadnego projektu', 'Projects management' => 'Zarządzanie projektami', @@ -1067,7 +1054,7 @@ return array( 'open' => 'otwarty', 'closed' => 'zamknięty', 'Priority:' => 'Priorytet:', - // 'Reference:' => '', + 'Reference:' => 'Odnośnik:', 'Complexity:' => 'Złożoność:', 'Swimlane:' => 'Proces:', 'Column:' => 'Kolumna:', @@ -1075,82 +1062,158 @@ return array( 'Creator:' => 'Utworzył:', 'Time estimated:' => 'Szacowany czas:', '%s hours' => '%s godzin', - 'Time spent:' => 'Wykorzystany czas:', + 'Time spent:' => 'Spędzony czas:', 'Created:' => 'Utworzone:', 'Modified:' => 'Zmodyfikowane:', 'Completed:' => 'Ukończone:', 'Started:' => 'Rozpoczęte:', - 'Moved:' => 'Przesunięcie:', + 'Moved:' => 'Przeniesione:', 'Task #%d' => 'Zadanie #%d', - // 'Date and time format' => '', - // 'Time format' => '', - // 'Start date: ' => '', - // 'End date: ' => '', - // 'New due date: ' => '', - // 'Start date changed: ' => '', - // 'Disable private projects' => '', - // 'Do you really want to remove this custom filter: "%s"?' => '', - // 'Remove a custom filter' => '', - // 'User activated successfully.' => '', - // 'Unable to enable this user.' => '', - // 'User disabled successfully.' => '', - // 'Unable to disable this user.' => '', - // 'All files have been uploaded successfully.' => '', - // 'View uploaded files' => '', - // 'The maximum allowed file size is %sB.' => '', - // 'Choose files again' => '', + 'Date and time format' => 'Format daty oraz czasu', + 'Time format' => 'Format czasu', + 'Start date: ' => 'Data rozpoczęcia: ', + 'End date: ' => 'Data zakończenia: ', + 'New due date: ' => 'Nowy termin: ', + 'Start date changed: ' => 'Data rozpoczęcia została zmieniona: ', + 'Disable private projects' => 'Wyłącz prywatne projekty', + 'Do you really want to remove this custom filter: "%s"?' => 'Na pewno usunąć niestandardowy filtr: "%s"?', + 'Remove a custom filter' => 'Usuń niestandardowy filtr', + 'User activated successfully.' => 'Użytkownik został aktywowany.', + 'Unable to enable this user.' => 'Nie można włączyć użytkownika.', + 'User disabled successfully.' => 'Użytkownik został wyłączony.', + 'Unable to disable this user.' => 'Nie można wyłączyć użytkownika.', + 'All files have been uploaded successfully.' => 'Wszystkie pliki zostały pomyślnie przesłane.', + 'View uploaded files' => 'Zobacz przesłane pliki', + 'The maximum allowed file size is %sB.' => 'Maksymalny rozmiar pliku to %sB.', + 'Choose files again' => 'Wybierz jeszcze raz pliki', 'Drag and drop your files here' => 'Przeciągnij i upuść pliki tutaj', 'choose files' => 'wybierz pliki', - // 'View profile' => '', - // 'Two Factor' => '', - // 'Disable user' => '', - // 'Do you really want to disable this user: "%s"?' => '', - // 'Enable user' => '', - // 'Do you really want to enable this user: "%s"?' => '', + 'View profile' => 'Zobacz profil', + 'Two Factor' => 'Dwustopniowe', + 'Disable user' => 'Wyłącz użytkownika', + 'Do you really want to disable this user: "%s"?' => 'Na pewno wyłączyć użytkownika: "%s"?', + 'Enable user' => 'Włącz użytkownika', + 'Do you really want to enable this user: "%s"?' => 'Na pewno włączyć użytkownika: "%s"?', 'Download' => 'Pobierz', 'Uploaded: %s' => 'Data załączenia: %s', 'Size: %s' => 'Rozmiar: %s', 'Uploaded by %s' => 'Załadowany przez %s', 'Filename' => 'Nazwa pliku', 'Size' => 'Rozmiar', - // 'Column created successfully.' => '', - // 'Another column with the same name exists in the project' => '', - // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', - // 'Change column position' => '', - // 'Switch to the project overview' => '', - // 'User filters' => '', - // 'Category filters' => '', + 'Column created successfully.' => 'Utworzono kolumnę.', + 'Another column with the same name exists in the project' => 'Inna kolumna o tej samej nazwie już istnieje w projekcie', + 'Default filters' => 'Domyślne filtry', + 'Your board doesn\'t have any columns!' => 'Twoja tablica nie ma żadnej kolumny!', + 'Change column position' => 'Zmień pozycję kolumny', + 'Switch to the project overview' => 'Przełącz do podsumowania projektu', + 'User filters' => 'Filtry użytkownika', + 'Category filters' => 'Filtry kategorii', 'Upload a file' => 'Prześlij plik', 'View file' => 'Wyświetl plik', 'Last activity' => 'Ostatnia aktywność', - // 'Change subtask position' => '', - // 'This value must be greater than %d' => '', - // 'Another swimlane with the same name exists in the project' => '', - // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', - // 'Actions duplicated successfully.' => '', - // 'Unable to duplicate actions.' => '', - // 'Add a new action' => '', - // 'Import from another project' => '', - // 'There is no action at the moment.' => '', - // 'Import actions from another project' => '', - // 'There is no available project.' => '', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', + 'Change subtask position' => 'Zmień pozycję pod-zadania', + 'This value must be greater than %d' => 'Wartość musi być większa niż %d', + 'Another swimlane with the same name exists in the project' => 'Inny tor o tej samej nazwie już istnieje w projekcie', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Przykład: http://example.kanboard.net/ (użyty do wygenerowania bezwzględnych adresów URL)', + 'Actions duplicated successfully.' => 'Pomyślnie zduplikowano akcje.', + 'Unable to duplicate actions.' => 'Nie można zduplikować akcji.', + 'Add a new action' => 'Dodaj nową akcję', + 'Import from another project' => 'Importuj z innego projektu', + 'There is no action at the moment.' => 'W chwili obecnej nie dodano żadnych akcji.', + 'Import actions from another project' => 'Importuj akcje z innego projektu', + 'There is no available project.' => 'Brak dostępnego projektu.', + 'Local File' => 'Plik lokalny', + 'Configuration' => 'Konfiguracja', + 'PHP version:' => 'Wersja PHP:', // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', - // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', + 'OS version:' => 'Wersja OS:', + 'Database version:' => 'Wersja bazy danych:', + 'Browser:' => 'Przeglądarka', + 'Task view' => 'Widok zadań', + 'Edit task' => 'Edytuj zadanie', + 'Edit description' => 'Edytuj opis', + 'New internal link' => 'Nowy wewnętrzny link', + 'Display list of keyboard shortcuts' => 'Wyświetl skróty klawiszowe', // 'Menu' => '', - // 'Set start date' => '', + 'Set start date' => 'Ustaw datę rozpoczęcia', // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'Upload my avatar image' => 'Prześlij avatar', + 'Remove my image' => 'Usuń', + 'The OAuth2 state parameter is invalid' => 'Parametr stanu OAuth2 jest niepoprawny', + 'User not found.' => 'Nie znaleziono użytkownika', + 'Search in activity stream' => 'Szukaj w strumieniu aktywności', + 'My activities' => 'Moje aktywności', + 'Activity until yesterday' => 'Aktywności do wczoraj', + 'Activity until today' => 'Aktywności do dzisiaj', + 'Search by creator: ' => 'Wyszukaj według twórcy: ', + 'Search by creation date: ' => 'Wyszukaj według daty utworzenia: ', + 'Search by task status: ' => 'Wyszukaj według statusu zadania: ', + 'Search by task title: ' => 'Wyszukaj po tytule zadania: ', + 'Activity stream search' => 'Wyszukaj w strumieniu aktywności', + 'Projects where "%s" is manager' => 'Projekty gdzie "%s" jest menedżerem', + 'Projects where "%s" is member' => 'Projekty gdzie "%s" jest uczestnikiem', + 'Open tasks assigned to "%s"' => 'Otwarte zadania przypisane do "%s"', + 'Closed tasks assigned to "%s"' => 'Zamknięte zadania przypisane do "%s"', + // 'Assign automatically a color based on a priority' => '', + 'Overdue tasks for the project(s) "%s"' => 'Zaległe zadania dla projektu/projektów "%s"', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/pt_BR/translations.php b/sources/app/Locale/pt_BR/translations.php index 050d1a9..343915b 100644 --- a/sources/app/Locale/pt_BR/translations.php +++ b/sources/app/Locale/pt_BR/translations.php @@ -35,7 +35,6 @@ return array( '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', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d tarefas finalizadas', 'No task for this project' => 'Não há tarefa para este projeto', 'Public link' => 'Link público', - 'Change assignee' => 'Alterar designação', - 'Change assignee for the task "%s"' => 'Alterar designação para a tarefa "%s"', 'Timezone' => 'Fuso horário', 'Sorry, I didn\'t find this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', 'Page not found' => 'Página não encontrada', @@ -170,7 +167,6 @@ return array( '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.', - 'Edit this task' => 'Editar esta tarefa', 'Due Date' => 'Data de vencimento', 'Invalid date' => 'Data inválida', 'Automatic actions' => 'Ações automáticas', @@ -250,7 +246,6 @@ return array( '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.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Você realmente deseja remover este arquivo: "%s"?', 'Attachments' => 'Anexos', 'Edit the task' => 'Editar a tarefa', - 'Edit the description' => 'Editar a descrição', 'Add a comment' => 'Adicionar um comentário', 'Edit a comment' => 'Editar um comentário', 'Summary' => 'Resumo', @@ -370,7 +364,6 @@ return array( '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', @@ -413,9 +406,7 @@ return array( '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', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Remover uma swimlane', 'Show default swimlane' => 'Exibir swimlane padrão', 'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projeto "%s"', - 'Swimlane not found.' => 'Swimlane não encontrada.', 'Swimlane removed successfully.' => 'Swimlane removida com sucesso.', 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane atualizada com sucesso.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Todas as swimlanes', 'All colors' => 'Todas as cores', '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"', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'ver o painel no Kanboard', 'The task have been moved to the first swimlane' => 'A tarefa foi movida para a primeira swimlane', 'The task have been moved to another swimlane:' => 'A tarefa foi movida para outra swimlane:', - 'Overdue tasks for the project "%s"' => 'Tarefas atrasadas para o projeto "%s"', 'New title: %s' => 'Novo título: %s', 'The task is not assigned anymore' => 'Agora a tarefa não está mais atribuída', 'New assignee: %s' => 'Novo designado: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Stop timer', 'Start timer' => 'Start timer', 'Add project member' => 'Adicionar membro ao projeto', - 'Enable notifications' => 'Ativar as notificações', 'My activity stream' => 'Meu feed de atividades', 'My calendar' => 'Minha agenda', 'Search tasks' => 'Pesquisar tarefas', @@ -1050,107 +1037,183 @@ return array( 'Add external link' => 'Adicionar um link externo', 'Type' => 'Tipo', 'Dependency' => 'Dependência', - // 'Add internal link' => '', - // 'Add a new external link' => '', - // 'Edit external link' => '', - // 'External link' => '', - // 'Copy and paste your link here...' => '', - // 'URL' => '', - // 'Internal links' => '', - // 'Assign to me' => '', - // 'Me' => '', - // 'Do not duplicate anything' => '', - // 'Projects management' => '', - // 'Users management' => '', - // 'Groups management' => '', - // 'Create from another project' => '', - // 'open' => '', - // 'closed' => '', - // 'Priority:' => '', - // 'Reference:' => '', - // 'Complexity:' => '', - // 'Swimlane:' => '', - // 'Column:' => '', - // 'Position:' => '', - // 'Creator:' => '', - // 'Time estimated:' => '', - // '%s hours' => '', - // 'Time spent:' => '', - // 'Created:' => '', - // 'Modified:' => '', - // 'Completed:' => '', - // 'Started:' => '', - // 'Moved:' => '', - // 'Task #%d' => '', - // 'Date and time format' => '', - // 'Time format' => '', - // 'Start date: ' => '', - // 'End date: ' => '', - // 'New due date: ' => '', - // 'Start date changed: ' => '', - // 'Disable private projects' => '', - // 'Do you really want to remove this custom filter: "%s"?' => '', - // 'Remove a custom filter' => '', - // 'User activated successfully.' => '', - // 'Unable to enable this user.' => '', - // 'User disabled successfully.' => '', - // 'Unable to disable this user.' => '', - // 'All files have been uploaded successfully.' => '', - // 'View uploaded files' => '', - // 'The maximum allowed file size is %sB.' => '', - // 'Choose files again' => '', - // 'Drag and drop your files here' => '', - // 'choose files' => '', - // 'View profile' => '', - // 'Two Factor' => '', - // 'Disable user' => '', - // 'Do you really want to disable this user: "%s"?' => '', - // 'Enable user' => '', - // 'Do you really want to enable this user: "%s"?' => '', - // 'Download' => '', - // 'Uploaded: %s' => '', - // 'Size: %s' => '', - // 'Uploaded by %s' => '', - // 'Filename' => '', - // 'Size' => '', - // 'Column created successfully.' => '', - // 'Another column with the same name exists in the project' => '', - // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', - // 'Change column position' => '', - // 'Switch to the project overview' => '', - // 'User filters' => '', - // 'Category filters' => '', - // 'Upload a file' => '', - // 'View file' => '', - // 'Last activity' => '', - // 'Change subtask position' => '', - // 'This value must be greater than %d' => '', - // 'Another swimlane with the same name exists in the project' => '', - // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '', - // 'Actions duplicated successfully.' => '', - // 'Unable to duplicate actions.' => '', - // 'Add a new action' => '', - // 'Import from another project' => '', - // 'There is no action at the moment.' => '', - // 'Import actions from another project' => '', - // 'There is no available project.' => '', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', - // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', - // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', - // 'Menu' => '', - // 'Set start date' => '', - // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'Add internal link' => 'Adicionar um link interno', + 'Add a new external link' => 'Adicionar um novo link externo', + 'Edit external link' => 'Editar um link externo', + 'External link' => 'Link externo', + 'Copy and paste your link here...' => 'Copie e cole o link aqui...', + 'URL' => 'URL', + 'Internal links' => 'Link interno', + 'Assign to me' => 'Atribuir-me', + 'Me' => 'Eu', + 'Do not duplicate anything' => 'Não duplique nada', + 'Projects management' => 'Gestão de projetos', + 'Users management' => 'Gestão dos usuários', + 'Groups management' => 'Gestão dos grupos', + 'Create from another project' => 'Criar a partir de outro projeto', + 'open' => 'aberto', + 'closed' => 'fechado', + 'Priority:' => 'Prioridade:', + 'Reference:' => 'Referência:', + 'Complexity:' => 'Complexidade:', + 'Swimlane:' => 'Swimlane:', + 'Column:' => 'Coluna:', + 'Position:' => 'Posição:', + 'Creator:' => 'Criador:', + 'Time estimated:' => 'Tempo estimado:', + '%s hours' => '%s horas', + 'Time spent:' => 'Tempo gasto:', + 'Created:' => 'Criado:', + 'Modified:' => 'Modificado:', + 'Completed:' => 'Completado:', + 'Started:' => 'Começado:', + 'Moved:' => 'Movido:', + 'Task #%d' => 'Tarefa #%d', + 'Date and time format' => 'Formato da hora e da data', + 'Time format' => 'Formato da hora', + 'Start date: ' => 'Data de início: ', + 'End date: ' => 'Data final: ', + 'New due date: ' => 'Nova data limite: ', + 'Start date changed: ' => 'Data de início alterada: ', + 'Disable private projects' => 'Desativar os projetos privados', + 'Do you really want to remove this custom filter: "%s"?' => 'Você realmente quer remover este filtro personalizado: "%s"?', + 'Remove a custom filter' => 'Remover um filtro personalizado', + 'User activated successfully.' => 'Usuário ativado com sucesso.', + 'Unable to enable this user.' => 'Impossível de ativar esse usuário.', + 'User disabled successfully.' => 'Usuário desactivado com sucesso.', + 'Unable to disable this user.' => 'Impossível de desativar esse usuário.', + 'All files have been uploaded successfully.' => 'Todos os arquivos foram enviados com sucesso.', + 'View uploaded files' => 'Ver os arquivos enviados', + 'The maximum allowed file size is %sB.' => 'O tamanho máximo dos arquivos é %sB.', + 'Choose files again' => 'Selecionar novamente arquivos', + 'Drag and drop your files here' => 'Arraste e solte os arquivos aqui', + 'choose files' => 'selecione os arquivos', + 'View profile' => 'Ver o perfil', + 'Two Factor' => 'Dois fatores', + 'Disable user' => 'Desativar o usuário', + 'Do you really want to disable this user: "%s"?' => 'Você realmente quer desativar este usuário: "%s"?', + 'Enable user' => 'Ativar um usuário', + 'Do you really want to enable this user: "%s"?' => 'Você realmente quer ativar este usuário: "%s"?', + 'Download' => 'Baixar', + 'Uploaded: %s' => 'Enviado: %s', + 'Size: %s' => 'Tamanho: %s', + 'Uploaded by %s' => 'Enviado por %s', + 'Filename' => 'Nome do arquivo', + 'Size' => 'Tamanho', + 'Column created successfully.' => 'A coluna criada com sucesso.', + 'Another column with the same name exists in the project' => 'Uma outra coluna com o mesmo nome já existe no projeto', + 'Default filters' => 'Filtros padrão', + 'Your board doesn\'t have any columns!' => 'O seu painel não tem nenhuma coluna', + 'Change column position' => 'Alterar a posição da coluna', + 'Switch to the project overview' => 'Mudar para a vista geral do projeto', + 'User filters' => 'Filtros dos usuários', + 'Category filters' => 'Filtros das categorias', + 'Upload a file' => 'Enviar um arquivo', + 'View file' => 'Ver um arquivo', + 'Last activity' => 'Últimas atividades', + 'Change subtask position' => 'Alterar a posição da sub-tarefa', + 'This value must be greater than %d' => 'Este valor deve ser maior que %d', + 'Another swimlane with the same name exists in the project' => 'Outra Swimlane existe com o mesmo nome no projeto', + 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Exemplo: http://exemple.kanboard.net/ (usado para gerar URLs absolutos)', + 'Actions duplicated successfully.' => 'Ações duplicadas com sucesso.', + 'Unable to duplicate actions.' => 'Não foi possível duplicar as ações.', + 'Add a new action' => 'Adicionar uma nova ação', + 'Import from another project' => 'Importar a partir de outro projeto', + 'There is no action at the moment.' => 'Não há nenhuma ação actualmente.', + 'Import actions from another project' => 'Importar ações a partir de outro projeto', + 'There is no available project.' => 'Não há projetos disponíveis.', + 'Local File' => 'Arquivo local', + 'Configuration' => 'Configuração', + 'PHP version:' => 'Versão do PHP:', + 'PHP SAPI:' => 'PHP SAPI:', + 'OS version:' => 'Versão do sistema operacional:', + 'Database version:' => 'Versão do banco de dados:', + 'Browser:' => 'Browser:', + 'Task view' => 'Vista detalhada de uma tarefa', + 'Edit task' => 'Editar a tarefa', + 'Edit description' => 'Editar a descrição', + 'New internal link' => 'Novo link interno', + 'Display list of keyboard shortcuts' => 'Ver a lista dos atalhos de teclado', + 'Menu' => 'Menu', + 'Set start date' => 'Definir a data de início', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Enviar a minha imagem de avatar', + 'Remove my image' => 'Remover a minha imagem', + 'The OAuth2 state parameter is invalid' => 'O parâmetro "state" de OAuth2 não é válido', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/pt_PT/translations.php b/sources/app/Locale/pt_PT/translations.php index 1c32788..d9ea27b 100644 --- a/sources/app/Locale/pt_PT/translations.php +++ b/sources/app/Locale/pt_PT/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Ver esta tarefa', 'Remove user' => 'Remover utilizador', 'Do you really want to remove this user: "%s"?' => 'Pretende mesmo remover este utilizador: "%s"?', - 'New user' => 'Novo utilizador', 'All users' => 'Todos os utilizadores', 'Username' => 'Nome de utilizador', 'Password' => 'Senha', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d tarefas finalizadas', 'No task for this project' => 'Não há tarefa para este projecto', 'Public link' => 'Link público', - 'Change assignee' => 'Mudar a assignação', - 'Change assignee for the task "%s"' => 'Modificar assignaçã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 na minha base de dados!', 'Page not found' => 'Página não encontrada', @@ -170,7 +167,6 @@ return array( '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.', - 'Edit this task' => 'Editar esta tarefa', 'Due Date' => 'Data de vencimento', 'Invalid date' => 'Data inválida', 'Automatic actions' => 'Acções automáticas', @@ -250,7 +246,6 @@ return array( 'Category' => 'Categoria', 'Category:' => 'Categoria:', 'Categories' => 'Categorias', - 'Category not found.' => 'Categoria não encontrada.', 'Your category have been created successfully.' => 'A 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 actualizada com sucesso.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Tem a certeza que quer remover este arquivo: "%s"', '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', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Nenhuma autenticação externa activa.', 'Password modified successfully.' => 'Senha alterada com sucesso.', 'Unable to change the password.' => 'Não foi possível alterar a senha.', - 'Change category for the task "%s"' => 'Mudar categoria da tarefa "%s"', 'Change category' => 'Mudar categoria', '%s updated the task %s' => '%s actualizou a tarefa %s', '%s opened the task %s' => '%s abriu a tarefa %s', @@ -413,9 +406,7 @@ return array( 'About' => 'Sobre', 'Database driver:' => 'Driver da base de dados:', 'Board settings' => 'Configurações do Quadro', - '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' => 'Redefinir token', 'API endpoint:' => 'API endpoint:', 'Refresh interval for private board' => 'Intervalo de actualização para um quadro privado', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Remover um swimlane', 'Show default swimlane' => 'Mostrar swimlane padrão', 'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projecto "%s"', - 'Swimlane not found.' => 'Swimlane não encontrado.', 'Swimlane removed successfully.' => 'Swimlane removido com sucesso.', 'Swimlanes' => 'Swimlanes', 'Swimlane updated successfully.' => 'Swimlane atualizado com sucesso.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Todos os swimlane', 'All colors' => 'Todas as cores', 'Moved to column %s' => 'Mover para a coluna %s', - 'Change description' => 'Modificar a descrição', 'User dashboard' => 'Painel de Controlo do utilizador', 'Allow only one subtask in progress at the same time for a user' => 'Permitir apenas uma subtarefa em andamento ao mesmo tempo para um utilizador', 'Edit column "%s"' => 'Editar a coluna "%s"', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'ver o painel no Kanboard', 'The task have been moved to the first swimlane' => 'A tarefa foi movida para o primeiro Swimlane', 'The task have been moved to another swimlane:' => 'A tarefa foi movida para outro Swimlane:', - 'Overdue tasks for the project "%s"' => 'Tarefas atrasadas para o projecto "%s"', 'New title: %s' => 'Novo título: %s', 'The task is not assigned anymore' => 'Tarefa já não está atribuída', 'New assignee: %s' => 'Novo assignado: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Parar temporizador', 'Start timer' => 'Iniciar temporizador', 'Add project member' => 'Adicionar um membro ao projecto', - 'Enable notifications' => 'Activar as notificações', 'My activity stream' => 'O meu feed de actividade', 'My calendar' => 'A minha agenda', 'Search tasks' => 'Pesquisar tarefas', @@ -852,7 +839,7 @@ return array( 'Gantt' => 'Gantt', 'Author' => 'Autor', 'Version' => 'Versão', - 'Plugins' => 'Extras', + 'Plugins' => 'Plugins', 'There is no plugin loaded.' => 'Não existem extras carregados', 'Set maximum column height' => 'Definir altura máxima da coluna', 'Remove maximum column height' => 'Remover altura máxima da coluna', @@ -1116,7 +1103,7 @@ return array( 'Column created successfully.' => 'Coluna criada com sucesso.', 'Another column with the same name exists in the project' => 'Já existe outra coluna com o mesmo nome no projecto', 'Default filters' => 'Filtros padrão', - 'Your board doesn\'t have any column!' => 'O seu quadro não tem nenhuma coluna!', + 'Your board doesn\'t have any columns!' => 'O seu quadro não tem nenhuma coluna!', 'Change column position' => 'Mudar posição da coluna', 'Switch to the project overview' => 'Mudar para vista geral do projecto', 'User filters' => 'Filtros de utilizador', @@ -1135,22 +1122,98 @@ return array( 'There is no action at the moment.' => 'De momento não existe acção.', 'Import actions from another project' => 'Importar acções de outro projecto', 'There is no available project.' => 'Não existe projecto disponivel.', - // 'Local File' => '', - // 'Configuration' => '', - // 'PHP version:' => '', - // 'PHP SAPI:' => '', - // 'OS version:' => '', - // 'Database version:' => '', - // 'Browser:' => '', - // 'Task view' => '', - // 'Edit task' => '', - // 'Edit description' => '', - // 'New internal link' => '', - // 'Display list of keyboard shortcuts' => '', - // 'Menu' => '', - // 'Set start date' => '', - // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'Local File' => 'Ficheiro Local', + 'Configuration' => 'Configuração', + 'PHP version:' => 'Versão PHP:', + 'PHP SAPI:' => 'SAPI PHP:', + 'OS version:' => 'Versão SO:', + 'Database version:' => 'Versão base de dados:', + 'Browser:' => 'Navegador:', + 'Task view' => 'Vista de Tarefas', + 'Edit task' => 'Editar tarefa', + 'Edit description' => 'Editar descrição', + 'New internal link' => 'Nova ligação interna', + 'Display list of keyboard shortcuts' => 'Mostrar lista de atalhos do teclado', + 'Menu' => 'Menu', + 'Set start date' => 'Definir data de inicio', + 'Avatar' => 'Avatar', + 'Upload my avatar image' => 'Enviar a minha imagem de avatar', + 'Remove my image' => 'Remover a minha imagem', + 'The OAuth2 state parameter is invalid' => 'O parametro de estado do OAuth2 é inválido', + 'User not found.' => 'Utilizador não encontrado.', + 'Search in activity stream' => 'Procurar no fluxo de atividade', + 'My activities' => 'Minhas actividades', + 'Activity until yesterday' => 'Actividade até ontem', + 'Activity until today' => 'Actividade até hoje', + 'Search by creator: ' => 'Procurar por criador: ', + 'Search by creation date: ' => 'Procurar por data de criação: ', + 'Search by task status: ' => 'Procurar por estado da tarefa: ', + 'Search by task title: ' => 'Procurar por titulo da tarefa: ', + 'Activity stream search' => 'Procurar fluxo de actividade', + 'Projects where "%s" is manager' => 'Projectos onde "%s" é gestor', + 'Projects where "%s" is member' => 'Projectos onde "%s" é membro', + 'Open tasks assigned to "%s"' => 'Tarefas abertas assignadas a "%s"', + 'Closed tasks assigned to "%s"' => 'Tarefas fechadas assignadas a "%s"', + 'Assign automatically a color based on a priority' => 'Assignar uma cor automáticamente de acordo com a prioridade', + 'Overdue tasks for the project(s) "%s"' => 'Tarefas em atraso para o(s) projecto(s) "%s"', + 'Upload files' => 'Enviar ficheiros', + 'Installed Plugins' => 'Plugins Instalados', + 'Plugin Directory' => 'Directoria de Plugins', + 'Plugin installed successfully.' => 'Plugin instalado com sucesso.', + 'Plugin updated successfully.' => 'Plugin actualizado com sucesso.', + 'Plugin removed successfully.' => 'Plugin removido com sucesso.', + 'Subtask converted to task successfully.' => 'Sub-tarefa convertida para tarefa com sucesso.', + 'Unable to convert the subtask.' => 'Não foi possivel converter a sub-tarefa.', + 'Unable to extract plugin archive.' => 'Não foi possivel extrair arquivo do plugin.', + 'Plugin not found.' => 'Plugin não encontrado.', + 'You don\'t have the permission to remove this plugin.' => 'Não tem acesso para remover este plugin.', + 'Unable to download plugin archive.' => 'Não foi possivel transferir o arquivo do plugin.', + 'Unable to write temporary file for plugin.' => 'Não foi possivel escrever o ficheiro temporário para o plugin.', + 'Unable to open plugin archive.' => 'Não foi possivel abrir o arquivo do plugin.', + 'There is no file in the plugin archive.' => 'Ficheiro não encontrado dentro do arquivo do plugin.', + 'Create tasks in bulk' => 'Criar tarefas em massa', + 'Your Kanboard instance is not configured to install plugins from the user interface.' => 'Este Kanboard não está configurado para instalar plugins através do interface de utilizador.', + 'There is no plugin available.' => 'Não existe nenhum plugin instalado.', + 'Install' => 'Instalar', + 'Update' => 'Actualizar', + 'Up to date' => 'Actualizado', + 'Not available' => 'Não disponivel', + 'Remove plugin' => 'Remover plugin', + 'Do you really want to remove this plugin: "%s"?' => 'Tem a certeza que pretende remover este plugin: "%s%"?', + 'Uninstall' => 'Desinstalar', + 'Listing' => 'A Listar', + 'Metadata' => 'Metadata', + 'Manage projects' => 'Gerir projectos', + 'Convert to task' => 'Converter para tarefa', + 'Convert sub-task to task' => 'Converter sub-tarefa para tarefa', + 'Do you really want to convert this sub-task to a task?' => 'Tem a certeza que pretende converter esta sub-tarefa para tarefa?', + 'My task title' => 'Titulo da minha tarefa', + 'Enter one task by line.' => 'Escreva uma tarefa por linha.', + 'Number of failed login:' => 'Número de logins falhados:', + 'Account locked until:' => 'Conta bloqueada até:', + 'Email settings' => 'Definições de Email', + 'Email sender address' => 'Endereço de envido de Email', + 'Email transport' => 'Transportador de Email', + 'Webhook token' => 'Token do Webhook', + 'Imports' => 'Importados', + 'Project tags management' => 'Gestão de etiquetas do Projecto', + 'Tag created successfully.' => 'Etiqueta criada com sucesso.', + 'Unable to create this tag.' => 'Não foi possivel criar esta etiqueta.', + 'Tag updated successfully.' => 'Etiqueta actualizada com sucesso.', + 'Unable to update this tag.' => 'Não foi possivel actualizar esta etiqueta.', + 'Tag removed successfully.' => 'Etiqueta removida com sucesso.', + 'Unable to remove this tag.' => 'Não foi possivel remover esta etiqueta.', + 'Global tags management' => 'Gestão de etiquetas globais', + 'Tags' => 'Etiquetas', + 'Tags management' => 'Gestão de Etiquetas', + 'Add new tag' => 'Adicionar etiqueta nova', + 'Edit a tag' => 'Editar a etiqueta', + 'Project tags' => 'Etiquetas do Projecto', + 'There is no specific tag for this project at the moment.' => 'De momento não existe nenhuma etiqueta para este projecto.', + 'Tag' => 'Etiqueta', + 'Remove a tag' => 'Remover etiqueta', + 'Do you really want to remove this tag: "%s"?' => 'Tem a certeza que pretende remover esta etiqueta: "%s"?', + 'Global tags' => 'Etiquetas globais', + 'There is no global tag at the moment.' => 'De momento não existe nenhuma etiqueta global.', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/ru_RU/translations.php b/sources/app/Locale/ru_RU/translations.php index 3cb3c6b..3b5e09d 100644 --- a/sources/app/Locale/ru_RU/translations.php +++ b/sources/app/Locale/ru_RU/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Посмотреть задачу', 'Remove user' => 'Удалить пользователя', 'Do you really want to remove this user: "%s"?' => 'Вы точно хотите удалить пользователя: « %s » ?', - 'New user' => 'Новый пользователь', 'All users' => 'Все пользователи', 'Username' => 'Имя пользователя', 'Password' => 'Пароль', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d завершенных задач', 'No task for this project' => 'Нет задач для этого проекта', 'Public link' => 'Ссылка для просмотра', - 'Change assignee' => 'Сменить назначенного', - 'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »', 'Timezone' => 'Часовой пояс', 'Sorry, I didn\'t find this information in my database!' => 'К сожалению, информация в базе данных не найдена !', 'Page not found' => 'Страница не найдена', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Напишите описание', 'Comment added successfully.' => 'Комментарий успешно добавлен.', 'Unable to create your comment.' => 'Невозможно создать комментарий.', - 'Edit this task' => 'Изменить задачу', 'Due Date' => 'Сделать до', 'Invalid date' => 'Неверная дата', 'Automatic actions' => 'Автоматические действия', @@ -250,7 +246,6 @@ return array( 'Category' => 'Категория', 'Category:' => 'Категория:', 'Categories' => 'Категории', - 'Category not found.' => 'Категория не найдена', 'Your category have been created successfully.' => 'Категория создана.', 'Unable to create your category.' => 'Не удалось создать категорию.', 'Your category have been updated successfully.' => 'Категория обновлена.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Вы точно хотите удалить этот файл « %s » ?', 'Attachments' => 'Вложения', 'Edit the task' => 'Изменить задачу', - 'Edit the description' => 'Изменить описание', 'Add a comment' => 'Добавить комментарий', 'Edit a comment' => 'Изменить комментарий', 'Summary' => 'Сводка', @@ -370,7 +364,6 @@ return array( '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', @@ -413,9 +406,7 @@ return array( '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' => 'Период обновления для частных досок', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Удалить дорожку', 'Show default swimlane' => 'Показать стандартную дорожку', 'Swimlane modification for the project "%s"' => 'Редактирование дорожки для проекта "%s"', - 'Swimlane not found.' => 'Дорожка не найдена.', 'Swimlane removed successfully.' => 'Дорожка успешно удалена', 'Swimlanes' => 'Дорожки', 'Swimlane updated successfully.' => 'Дорожка успешно обновлена.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Все дорожки', 'All colors' => 'Все цвета', 'Moved to column %s' => 'Перемещена в колонку %s', - 'Change description' => 'Изменить описание', 'User dashboard' => 'Пользователь панели мониторинга', 'Allow only one subtask in progress at the same time for a user' => 'Разрешена только одна подзадача в разработке одновременно для одного пользователя', 'Edit column "%s"' => 'Редактировать колонку "%s"', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'посмотреть доску на Kanboard', 'The task have been moved to the first swimlane' => 'Эта задача была перемещена в первую дорожку', 'The task have been moved to another swimlane:' => 'Эта задача была перемещена в другую дорожку:', - 'Overdue tasks for the project "%s"' => 'Просроченные задачи для проекта "%s"', 'New title: %s' => 'Новый заголовок: %s', 'The task is not assigned anymore' => 'Задача больше не назначена', 'New assignee: %s' => 'Новый назначенный: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Остановить таймер', 'Start timer' => 'Запустить таймер', 'Add project member' => 'Добавить номер проекта', - 'Enable notifications' => 'Включить уведомления', 'My activity stream' => 'Лента моей активности', 'My calendar' => 'Мой календарь', 'Search tasks' => 'Поиск задачи', @@ -1116,7 +1103,7 @@ return array( 'Column created successfully.' => 'Столбец успешно создан', 'Another column with the same name exists in the project' => 'Столбец с таким именем уже существует в этом проекте', 'Default filters' => 'Стандартные фильтры', - 'Your board doesn\'t have any column!' => 'Ваша доска не имеет ни одного столбца!', + 'Your board doesn\'t have any columns!' => 'Ваша доска не имеет ни одного столбца!', 'Change column position' => 'Смена позиции столбца', 'Switch to the project overview' => 'Переключение на обзор проекта', 'User filters' => 'Фильтры по пользователям', @@ -1149,8 +1136,84 @@ return array( 'Display list of keyboard shortcuts' => 'Показать список клавиатурных сокращений', 'Menu' => 'Меню', 'Set start date' => 'Установить дату начала', - // 'Avatar' => '', - // 'Upload my avatar image' => '', - // 'Remove my image' => '', - // 'The OAuth2 state parameter is invalid' => '', + 'Avatar' => 'Аватар', + 'Upload my avatar image' => 'Загрузить моё изображение для аватара', + 'Remove my image' => 'Удалить моё изображение', + 'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный', + 'User not found.' => 'Пользователь не найден', + 'Search in activity stream' => 'Поиск в потоке активности', + 'My activities' => 'Мои активности', + 'Activity until yesterday' => 'Активности до вчерашнего дня', + 'Activity until today' => 'Активности до сегодня', + 'Search by creator: ' => 'Поиск по создателю: ', + 'Search by creation date: ' => 'Поиск по дате создания: ', + 'Search by task status: ' => 'Поиск по статусу задачи: ', + 'Search by task title: ' => 'Поиск по заголоску задачи: ', + 'Activity stream search' => 'Поиск в потоке активности; ', + 'Projects where "%s" is manager' => 'Проекты, где менеджером является "%s"', + 'Projects where "%s" is member' => 'Проекты, где членом является "%s"', + 'Open tasks assigned to "%s"' => 'Открытые задачи, назначенные на "%s"', + 'Closed tasks assigned to "%s"' => 'Закрытые задачи, назначенные на "%s"', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/sr_Latn_RS/translations.php b/sources/app/Locale/sr_Latn_RS/translations.php index c7070a8..00e607f 100644 --- a/sources/app/Locale/sr_Latn_RS/translations.php +++ b/sources/app/Locale/sr_Latn_RS/translations.php @@ -35,7 +35,6 @@ return array( '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', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d zatvorenih zadataka', 'No task for this project' => 'Nema dodeljenih zadataka ovom projektu', 'Public link' => 'Javni link', - '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', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Dodaj opis', 'Comment added successfully.' => 'Komentar uspešno ostavljen', 'Unable to create your comment.' => 'Nemoguće kreiranje komentara', - 'Edit this task' => 'Izmeni ovaj zadatak', 'Due Date' => 'Termin', 'Invalid date' => 'Loš datum', 'Automatic actions' => 'Automatske akcije', @@ -250,7 +246,6 @@ return array( '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', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Da li da uklonim fajl: "%s"?', 'Attachments' => 'Prilozi', 'Edit the task' => 'Izmena Zadatka', - 'Edit the description' => 'Izmena opisa', 'Add a comment' => 'Dodaj komentar', 'Edit a comment' => 'Izmeni komentar', 'Summary' => 'Pregled', @@ -370,7 +364,6 @@ return array( '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', @@ -413,9 +406,7 @@ return array( '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', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Ukloni razdelnik', '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.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Svi razdelniki', 'All colors' => 'Sve boje', '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"' => '', @@ -709,7 +698,6 @@ return array( // 'view the board on Kanboard' => '', // 'The task have been moved to the first swimlane' => '', // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', // 'New title: %s' => '', // 'The task is not assigned anymore' => '', // 'New assignee: %s' => '', @@ -738,7 +726,6 @@ return array( // 'Stop timer' => '', // 'Start timer' => '', // 'Add project member' => '', - // 'Enable notifications' => '', // 'My activity stream' => '', // 'My calendar' => '', // 'Search tasks' => '', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/sv_SE/translations.php b/sources/app/Locale/sv_SE/translations.php index e4728d2..88db5d6 100644 --- a/sources/app/Locale/sv_SE/translations.php +++ b/sources/app/Locale/sv_SE/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'Se denna uppgift', 'Remove user' => 'Ta bort användare', 'Do you really want to remove this user: "%s"?' => 'Vill du verkligen ta bort användaren: "%s"?', - 'New user' => 'Ny användare', 'All users' => 'Alla användare', 'Username' => 'Användarnamn', 'Password' => 'Lösenord', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d stängda uppgifter', 'No task for this project' => 'Inga uppgifter i detta projekt', 'Public link' => 'Publik länk', - 'Change assignee' => 'Ändra uppdragsinnehavare', - 'Change assignee for the task "%s"' => 'Ändra uppdragsinnehavare för uppgiften "%s"', 'Timezone' => 'Tidszon', 'Sorry, I didn\'t find this information in my database!' => 'Informationen kunde inte hittas i databasen.', 'Page not found' => 'Sidan hittas inte', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Lämna en beskrivning', 'Comment added successfully.' => 'Kommentaren har lagts till.', 'Unable to create your comment.' => 'Kommentaren kunde inte laddas upp.', - 'Edit this task' => 'Ändra denna uppgift', 'Due Date' => 'Måldatum', 'Invalid date' => 'Ej tillåtet datum', 'Automatic actions' => 'Automatiska åtgärder', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategori', 'Category:' => 'Kategori:', 'Categories' => 'Ange kategorier', - 'Category not found.' => 'Kategorin hittades inte', 'Your category have been created successfully.' => 'Din kategori har skapats', 'Unable to create your category.' => 'Kunde inte skapa din kategori', 'Your category have been updated successfully.' => 'Din kategori har uppdaterats', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Vill du verkligen ta bort denna fil:"%s"?', 'Attachments' => 'Bifogade filer', 'Edit the task' => 'Ändra uppgiften', - 'Edit the description' => 'Ändra beskrivningen', 'Add a comment' => 'Lägg till kommentar', 'Edit a comment' => 'Ändra en kommentar', 'Summary' => 'Sammanfattning', @@ -370,7 +364,6 @@ return array( 'No external authentication enabled.' => 'Ingen extern autentisering aktiverad.', 'Password modified successfully.' => 'Lösenordet har ändrats.', 'Unable to change the password.' => 'Kunde inte byta lösenord.', - 'Change category for the task "%s"' => 'Byt kategori för uppgiften "%s"', 'Change category' => 'Byt kategori', '%s updated the task %s' => '%s uppdaterade uppgiften %s', '%s opened the task %s' => '%s öppna uppgiften %s', @@ -413,9 +406,7 @@ return array( 'About' => 'Om', 'Database driver:' => 'Databasdrivrutin:', 'Board settings' => 'Inställningar för tavla', - 'URL and token' => 'URL och token', 'Webhook settings' => 'Webhook inställningar', - 'URL for task creation:' => 'URL för att skapa uppgift:', 'Reset token' => 'Nollställ token', 'API endpoint:' => 'API ändpunkt:', 'Refresh interval for private board' => 'Uppdateringsintervall för privat tavla', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Ta bort en swimlane', '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', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Alla swimlanes', 'All colors' => 'Alla färger', '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"', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'visa tavlan på Kanboard', 'The task have been moved to the first swimlane' => 'Uppgiften har flyttats till första swimlane', 'The task have been moved to another swimlane:' => 'Uppgiften har flyttats till en annan swimlane:', - 'Overdue tasks for the project "%s"' => 'Försenade uppgifter för projektet "%s"', 'New title: %s' => 'Ny titel: %s', 'The task is not assigned anymore' => 'Uppgiften är inte länge tilldelad', 'New assignee: %s' => 'Ny tilldelning: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Stoppa timer', 'Start timer' => 'Starta timer', 'Add project member' => 'Lägg till projektmedlem', - 'Enable notifications' => 'Aktivera notiser', 'My activity stream' => 'Min aktivitetsström', 'My calendar' => 'Min kalender', 'Search tasks' => 'Sök uppgifter', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/th_TH/translations.php b/sources/app/Locale/th_TH/translations.php index 1e2fb98..fd35d28 100644 --- a/sources/app/Locale/th_TH/translations.php +++ b/sources/app/Locale/th_TH/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => 'รายละเอียดงานนี้', 'Remove user' => 'เอาผู้ใช้ออก', 'Do you really want to remove this user: "%s"?' => 'คุณต้องการเอาผู้ใช้ « %s » ออกใช่หรือไม่?', - 'New user' => 'ผู้ใช้ใหม่', 'All users' => 'ผู้ใช้ทั้งหมด', 'Username' => 'ชื่อผู้ใช้', 'Password' => 'รหัสผ่าน', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d งานที่ปิด', 'No task for this project' => 'ไม่มีงานสำหรับโปรเจคนี้', 'Public link' => 'ลิงค์สาธารณะ', - 'Change assignee' => 'เปลี่ยนการกำหนด', - 'Change assignee for the task "%s"' => 'เปลี่ยนการกำหนดสำหรับงาน « %s »', 'Timezone' => 'เขตเวลา', 'Sorry, I didn\'t find this information in my database!' => 'เสียใจด้วย ไม่สามารถหาข้อมูลในฐานข้อมูลได้', 'Page not found' => 'ไม่พบหน้า', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'แสดงคำอธิบาย', 'Comment added successfully.' => 'เพิ่มความคิดเห็นเรียบร้อยแล้ว', 'Unable to create your comment.' => 'ไม่สามารถสร้างความคิดเห็น', - 'Edit this task' => 'แก้ไขงาน', 'Due Date' => 'วันที่ครบกำหนด', 'Invalid date' => 'วันที่ผิด', 'Automatic actions' => 'การกระทำอัตโนมัติ', @@ -250,7 +246,6 @@ return array( 'Category' => 'หมวด', 'Category:' => 'หมวด:', 'Categories' => 'หมวด', - 'Category not found.' => 'ไม่พบหมวด', 'Your category have been created successfully.' => 'สร้างหมวดเรียบร้อยแล้ว', 'Unable to create your category.' => 'ไม่สามารถสร้างหมวดได้', 'Your category have been updated successfully.' => 'ปรับปรุงหมวดเรียบร้อยแล้ว', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'คุณต้องการลบไฟล์ "%s" ใช่หรือไม่?', 'Attachments' => 'แนบ', 'Edit the task' => 'แก้ไขงาน', - 'Edit the description' => 'แก้ไขคำอธิบาย', 'Add a comment' => 'เพิ่มความคิดเห็น', 'Edit a comment' => 'แก้ไขความคิดเห็น', 'Summary' => 'สรุป', @@ -370,7 +364,6 @@ return array( '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', @@ -413,9 +406,7 @@ return array( 'About' => 'เกี่ยวกับ', 'Database driver:' => 'เครื่องมือฐานขข้อมูล', 'Board settings' => 'ตั้งค่าบอร์ด', - // 'URL and token' => '', // 'Webhook settings' => '', - // 'URL for task creation:' => '', // 'Reset token' => '', // 'API endpoint:' => '', 'Refresh interval for private board' => 'ระยะรีเฟรชบอร์ดส่วนตัว', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'ลบสวิมเลน', 'Show default swimlane' => 'แสดงสวิมเลนเริ่มต้น', 'Swimlane modification for the project "%s"' => 'แก้ไขสวิมเลนสำหรับโปรเจค "%s"', - 'Swimlane not found.' => 'หาสวิมเลนไม่พบ', 'Swimlane removed successfully.' => 'ลบสวิมเลนเรียบร้อยแล้ว', 'Swimlanes' => 'สวิมเลน', 'Swimlane updated successfully.' => 'ปรับปรุงสวิมเลนเรียบร้อยแล้ว', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'สวิมเลนทั้งหมด', 'All colors' => 'สีทั้งหมด', 'Moved to column %s' => 'เคลื่อนไปคอลัมน์ %s', - 'Change description' => 'เปลี่ยนคำอธิบาย', 'User dashboard' => 'ผู้ใช้แดชบอร์ด', 'Allow only one subtask in progress at the same time for a user' => 'อนุญาตให้ทำงานย่อยได้เพียงงานเดียวต่อหนึ่งคนในเวลาเดียวกัน', 'Edit column "%s"' => 'แก้ไขคอลัมน์ "%s"', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'แสดงบอร์ดบนคังบอร์ด', 'The task have been moved to the first swimlane' => 'งานถูกย้านไปสวิมเลนแรก', 'The task have been moved to another swimlane:' => 'งานถูกย้านไปสวิมเลนอื่น:', - 'Overdue tasks for the project "%s"' => 'งานที่เกินกำหนดสำหรับโปรเจค "%s"', 'New title: %s' => 'ชื่อเรื่องใหม่: %s', 'The task is not assigned anymore' => 'ไม่กำหนดผู้รับผิดชอบ', 'New assignee: %s' => 'ผู้รับผิดชอบใหม่: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'หยุดจับเวลา', 'Start timer' => 'เริ่มจับเวลา', 'Add project member' => 'เพิ่มสมาชิกโปรเจค', - 'Enable notifications' => 'เปิดการแจ้งเตือน', 'My activity stream' => 'กิจกรรมที่เกิดขึ้นของฉัน', 'My calendar' => 'ปฎิทินของฉัน', 'Search tasks' => 'ค้นหางาน', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/tr_TR/translations.php b/sources/app/Locale/tr_TR/translations.php index 6e8fae2..41e2259 100644 --- a/sources/app/Locale/tr_TR/translations.php +++ b/sources/app/Locale/tr_TR/translations.php @@ -35,7 +35,6 @@ return array( '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', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d kapatılmış görevler', 'No task for this project' => 'Bu proje için görev yok', 'Public link' => 'Dışa açık link', - 'Change assignee' => 'Atanmış Kullanıcıyı değiştir', - 'Change assignee for the task "%s"' => '"%s" görevi için atanmış kullanıcıyı değiştir', 'Timezone' => 'Saat dilimi', 'Sorry, I didn\'t find this information in my database!' => 'Üzgünüm, bu bilgiyi veri tabanımda bulamadım.', 'Page not found' => 'Sayfa bulunamadı', @@ -170,7 +167,6 @@ return array( 'Leave a description' => 'Açıklama ekleyin', 'Comment added successfully.' => 'Yorum eklendi', 'Unable to create your comment.' => 'Yorumunuz oluşturulamadı', - 'Edit this task' => 'Bu görevi değiştir', 'Due Date' => 'Bitiş Tarihi', 'Invalid date' => 'Geçersiz tarihi', 'Automatic actions' => 'Otomatik işlemler', @@ -250,7 +246,6 @@ return array( 'Category' => 'Kategori', 'Category:' => 'Kategori:', 'Categories' => 'Kategoriler', - 'Category not found.' => 'Kategori bulunamadı.', 'Your category have been created successfully.' => 'Kategori başarıyla oluşturuldu.', 'Unable to create your category.' => 'Kategori oluşturulamadı.', 'Your category have been updated successfully.' => 'Kategori başarıyla güncellendi.', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => 'Bu dosyayı silmek istediğinize emin misiniz: "%s"?', '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', @@ -370,7 +364,6 @@ return array( '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ştirilemiyor.', - 'Change category for the task "%s"' => '"%s" görevi için kategori değiştirme', 'Change category' => 'Kategori değiştirme', '%s updated the task %s' => '%s kullanıcısı %s görevini güncelledi', '%s opened the task %s' => '%s kullanıcısı %s görevini açtı', @@ -413,9 +406,7 @@ return array( 'About' => 'Hakkında', 'Database driver:' => 'Veritabanı sürücüsü:', 'Board settings' => 'Tablo ayarları', - 'URL and token' => 'URL veya Belirteç', 'Webhook settings' => 'Webhook ayarları', - 'URL for task creation:' => 'Görev oluşturma için URL', 'Reset token' => 'Belirteci sıfırla', 'API endpoint:' => 'API bitiş noktası:', 'Refresh interval for private board' => 'Özel tablolar için yenileme sıklığı', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => 'Kulvarı sil', '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.', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => 'Tüm Kulvarlar', 'All colors' => 'Tüm Renkler', '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', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => 'Tabloyu Kanboard\'da görüntüle', 'The task have been moved to the first swimlane' => 'Görev birinci kulvara taşındı', 'The task have been moved to another swimlane:' => 'Görev başka bir kulvara taşındı:', - 'Overdue tasks for the project "%s"' => '"%s" projesi için gecikmiş görevler', 'New title: %s' => 'Yeni başlık: %s', 'The task is not assigned anymore' => 'Görev artık atanmamış', 'New assignee: %s' => 'Yeni atanan: %s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => 'Zamanlayıcıyı durdur', 'Start timer' => 'Zamanlayıcıyı başlat', 'Add project member' => 'Proje üyesi ekle', - 'Enable notifications' => 'Bildirimleri etkinleştir', 'My activity stream' => 'Olay akışım', 'My calendar' => 'Takvimim', 'Search tasks' => 'Görevleri ara', @@ -1116,7 +1103,7 @@ return array( // 'Column created successfully.' => '', // 'Another column with the same name exists in the project' => '', // 'Default filters' => '', - // 'Your board doesn\'t have any column!' => '', + // 'Your board doesn\'t have any columns!' => '', // 'Change column position' => '', // 'Switch to the project overview' => '', // 'User filters' => '', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Locale/zh_CN/translations.php b/sources/app/Locale/zh_CN/translations.php index decd49d..2f456cc 100644 --- a/sources/app/Locale/zh_CN/translations.php +++ b/sources/app/Locale/zh_CN/translations.php @@ -35,7 +35,6 @@ return array( 'View this task' => '查看该任务', 'Remove user' => '移除用户', 'Do you really want to remove this user: "%s"?' => '确定要删除用户"%s"吗?', - 'New user' => '新建用户', 'All users' => '所有用户', 'Username' => '用户名', 'Password' => '密码', @@ -155,8 +154,6 @@ return array( '%d closed tasks' => '%d个已关闭任务', 'No task for this project' => '该项目尚无任务', 'Public link' => '公开链接', - 'Change assignee' => '变更负责人', - 'Change assignee for the task "%s"' => '更改任务"%s"的负责人', 'Timezone' => '时区', 'Sorry, I didn\'t find this information in my database!' => '抱歉,无法在数据库中找到该信息!', 'Page not found' => '页面未找到', @@ -170,7 +167,6 @@ return array( 'Leave a description' => '给一个描述', 'Comment added successfully.' => '评论成功添加。', 'Unable to create your comment.' => '无法创建评论。', - 'Edit this task' => '编辑该任务', 'Due Date' => '到期时间', 'Invalid date' => '无效日期', 'Automatic actions' => '自动动作', @@ -250,7 +246,6 @@ return array( 'Category' => '分类', 'Category:' => '分类:', 'Categories' => '分类', - 'Category not found.' => '未找到分类。', 'Your category have been created successfully.' => '成功为您创建分类。', 'Unable to create your category.' => '无法为您创建分类。', 'Your category have been updated successfully.' => '成功为您更新分类。', @@ -272,7 +267,6 @@ return array( 'Do you really want to remove this file: "%s"?' => '确定要移除文件"%s"吗?', 'Attachments' => '附件', 'Edit the task' => '修改任务', - 'Edit the description' => '修改描述', 'Add a comment' => '添加评论', 'Edit a comment' => '编辑评论', 'Summary' => '概要', @@ -370,7 +364,6 @@ return array( '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', @@ -413,9 +406,7 @@ return array( 'About' => '关于', 'Database driver:' => '数据库驱动:', 'Board settings' => '看板设置', - 'URL and token' => 'URL和令牌', 'Webhook settings' => 'Webhook 设置', - 'URL for task creation:' => '创建任务的URL:', 'Reset token' => '重置令牌', 'API endpoint:' => 'API 端点:', 'Refresh interval for private board' => '私人看板的刷新时间', @@ -489,7 +480,6 @@ return array( 'Remove a swimlane' => '删除里程碑', 'Show default swimlane' => '显示默认里程碑', 'Swimlane modification for the project "%s"' => '项目"%s"的里程碑变更', - 'Swimlane not found.' => '未找到里程碑。', 'Swimlane removed successfully.' => '成功删除里程碑', 'Swimlanes' => '里程碑', 'Swimlane updated successfully.' => '成功更新了里程碑。', @@ -517,7 +507,6 @@ return array( 'All swimlanes' => '全部里程碑', 'All colors' => '全部颜色', 'Moved to column %s' => '移动到栏目 %s', - 'Change description' => '修改描述', 'User dashboard' => '用户仪表板', 'Allow only one subtask in progress at the same time for a user' => '每用户同时仅有一个活动子任务', 'Edit column "%s"' => '编辑栏目"%s"', @@ -709,7 +698,6 @@ return array( 'view the board on Kanboard' => '在看板上查看面板', 'The task have been moved to the first swimlane' => '该任务已被移动到首个里程碑', 'The task have been moved to another swimlane:' => '该任务已被移动到别的里程碑:', - 'Overdue tasks for the project "%s"' => '"%s"项目下的超期任务', 'New title: %s' => '新标题:%s', 'The task is not assigned anymore' => '该任务没有指派人', 'New assignee: %s' => '新指派到:%s', @@ -738,7 +726,6 @@ return array( 'Stop timer' => '停止计时器', 'Start timer' => '开启计时器', 'Add project member' => '添加项目成员', - 'Enable notifications' => '打开通知', 'My activity stream' => '我的活动流', 'My calendar' => '我的日程表', 'Search tasks' => '搜索任务', @@ -1116,7 +1103,7 @@ return array( 'Column created successfully.' => '新增任务栏成功。', 'Another column with the same name exists in the project' => '当前项目中重名任务栏已存在', 'Default filters' => '默认过滤器', - 'Your board doesn\'t have any column!' => '你的看板没有任何栏目', + 'Your board doesn\'t have any columns!' => '你的看板没有任何栏目', 'Change column position' => '更改任务栏位置', 'Switch to the project overview' => '切换到项目视图', 'User filters' => '用户过滤器', @@ -1153,4 +1140,80 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', + // 'Projects where "%s" is manager' => '', + // 'Projects where "%s" is member' => '', + // 'Open tasks assigned to "%s"' => '', + // 'Closed tasks assigned to "%s"' => '', + // 'Assign automatically a color based on a priority' => '', + // 'Overdue tasks for the project(s) "%s"' => '', + // 'Upload files' => '', + // 'Installed Plugins' => '', + // 'Plugin Directory' => '', + // 'Plugin installed successfully.' => '', + // 'Plugin updated successfully.' => '', + // 'Plugin removed successfully.' => '', + // 'Subtask converted to task successfully.' => '', + // 'Unable to convert the subtask.' => '', + // 'Unable to extract plugin archive.' => '', + // 'Plugin not found.' => '', + // 'You don\'t have the permission to remove this plugin.' => '', + // 'Unable to download plugin archive.' => '', + // 'Unable to write temporary file for plugin.' => '', + // 'Unable to open plugin archive.' => '', + // 'There is no file in the plugin archive.' => '', + // 'Create tasks in bulk' => '', + // 'Your Kanboard instance is not configured to install plugins from the user interface.' => '', + // 'There is no plugin available.' => '', + // 'Install' => '', + // 'Update' => '', + // 'Up to date' => '', + // 'Not available' => '', + // 'Remove plugin' => '', + // 'Do you really want to remove this plugin: "%s"?' => '', + // 'Uninstall' => '', + // 'Listing' => '', + // 'Metadata' => '', + // 'Manage projects' => '', + // 'Convert to task' => '', + // 'Convert sub-task to task' => '', + // 'Do you really want to convert this sub-task to a task?' => '', + // 'My task title' => '', + // 'Enter one task by line.' => '', + // 'Number of failed login:' => '', + // 'Account locked until:' => '', + // 'Email settings' => '', + // 'Email sender address' => '', + // 'Email transport' => '', + // 'Webhook token' => '', + // 'Imports' => '', + // 'Project tags management' => '', + // 'Tag created successfully.' => '', + // 'Unable to create this tag.' => '', + // 'Tag updated successfully.' => '', + // 'Unable to update this tag.' => '', + // 'Tag removed successfully.' => '', + // 'Unable to remove this tag.' => '', + // 'Global tags management' => '', + // 'Tags' => '', + // 'Tags management' => '', + // 'Add new tag' => '', + // 'Edit a tag' => '', + // 'Project tags' => '', + // 'There is no specific tag for this project at the moment.' => '', + // 'Tag' => '', + // 'Remove a tag' => '', + // 'Do you really want to remove this tag: "%s"?' => '', + // 'Global tags' => '', + // 'There is no global tag at the moment.' => '', + // 'This field cannot be empty' => '', ); diff --git a/sources/app/Middleware/ApplicationAuthorizationMiddleware.php b/sources/app/Middleware/ApplicationAuthorizationMiddleware.php new file mode 100644 index 0000000..faca2d6 --- /dev/null +++ b/sources/app/Middleware/ApplicationAuthorizationMiddleware.php @@ -0,0 +1,27 @@ +helper->user->hasAccess($this->router->getController(), $this->router->getAction())) { + throw new AccessForbiddenException(); + } + + $this->next(); + } +} diff --git a/sources/app/Middleware/AuthenticationMiddleware.php b/sources/app/Middleware/AuthenticationMiddleware.php new file mode 100644 index 0000000..499843f --- /dev/null +++ b/sources/app/Middleware/AuthenticationMiddleware.php @@ -0,0 +1,56 @@ +authenticationManager->checkCurrentSession()) { + throw AccessForbiddenException::getInstance()->withoutLayout(); + } + + if (! $this->isPublicAccess()) { + $this->handleAuthentication(); + } + + $this->next(); + } + + protected function handleAuthentication() + { + if (! $this->userSession->isLogged() && ! $this->authenticationManager->preAuthentication()) { + $this->nextMiddleware = null; + + if ($this->request->isAjax()) { + $this->response->text('Not Authorized', 401); + } else { + $this->sessionStorage->redirectAfterLogin = $this->request->getUri(); + $this->response->redirect($this->helper->url->to('AuthController', 'login')); + } + } + } + + protected function isPublicAccess() + { + if ($this->applicationAuthorization->isAllowed($this->router->getController(), $this->router->getAction(), Role::APP_PUBLIC)) { + $this->nextMiddleware = null; + return true; + } + + return false; + } +} diff --git a/sources/app/Middleware/BootstrapMiddleware.php b/sources/app/Middleware/BootstrapMiddleware.php new file mode 100644 index 0000000..727f600 --- /dev/null +++ b/sources/app/Middleware/BootstrapMiddleware.php @@ -0,0 +1,44 @@ +sessionManager->open(); + $this->dispatcher->dispatch('app.bootstrap'); + $this->sendHeaders(); + $this->next(); + } + + /** + * Send HTTP headers + * + * @access private + */ + private function sendHeaders() + { + $this->response->withContentSecurityPolicy($this->container['cspRules']); + $this->response->withSecurityHeaders(); + + if (ENABLE_XFRAME) { + $this->response->withXframe(); + } + + if (ENABLE_HSTS) { + $this->response->withStrictTransportSecurity(); + } + } +} diff --git a/sources/app/Middleware/PostAuthenticationMiddleware.php b/sources/app/Middleware/PostAuthenticationMiddleware.php new file mode 100644 index 0000000..f7eccbc --- /dev/null +++ b/sources/app/Middleware/PostAuthenticationMiddleware.php @@ -0,0 +1,36 @@ +router->getController()); + $action = strtolower($this->router->getAction()); + $ignore = ($controller === 'twofactorcontroller' && in_array($action, array('code', 'check'))) || ($controller === 'authcontroller' && $action === 'logout'); + + if ($ignore === false && $this->userSession->hasPostAuthentication() && ! $this->userSession->isPostAuthenticationValidated()) { + $this->nextMiddleware = null; + + if ($this->request->isAjax()) { + $this->response->text('Not Authorized', 401); + } + + $this->response->redirect($this->helper->url->to('TwoFactorController', 'code')); + } + + $this->next(); + } +} diff --git a/sources/app/Middleware/ProjectAuthorizationMiddleware.php b/sources/app/Middleware/ProjectAuthorizationMiddleware.php new file mode 100644 index 0000000..704491b --- /dev/null +++ b/sources/app/Middleware/ProjectAuthorizationMiddleware.php @@ -0,0 +1,34 @@ +request->getIntegerParam('project_id'); + $task_id = $this->request->getIntegerParam('task_id'); + + if ($task_id > 0 && $project_id === 0) { + $project_id = $this->taskFinderModel->getProjectId($task_id); + } + + if ($project_id > 0 && ! $this->helper->user->hasProjectAccess($this->router->getController(), $this->router->getAction(), $project_id)) { + throw new AccessForbiddenException(); + } + + $this->next(); + } +} diff --git a/sources/app/Model/Action.php b/sources/app/Model/ActionModel.php similarity index 79% rename from sources/app/Model/Action.php rename to sources/app/Model/ActionModel.php index f055d9d..b5d2bd0 100644 --- a/sources/app/Model/Action.php +++ b/sources/app/Model/ActionModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Action Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Action extends Base +class ActionModel extends Base { /** * SQL table name for actions @@ -26,12 +28,12 @@ class Action extends Base */ public function getAllByUser($user_id) { - $project_ids = $this->projectPermission->getActiveProjectIds($user_id); + $project_ids = $this->projectPermissionModel->getActiveProjectIds($user_id); $actions = array(); if (! empty($project_ids)) { $actions = $this->db->table(self::TABLE)->in('project_id', $project_ids)->findAll(); - $params = $this->actionParameter->getAllByActions(array_column($actions, 'id')); + $params = $this->actionParameterModel->getAllByActions(array_column($actions, 'id')); $this->attachParamsToActions($actions, $params); } @@ -48,7 +50,7 @@ class Action extends Base public function getAllByProject($project_id) { $actions = $this->db->table(self::TABLE)->eq('project_id', $project_id)->findAll(); - $params = $this->actionParameter->getAllByActions(array_column($actions, 'id')); + $params = $this->actionParameterModel->getAllByActions(array_column($actions, 'id')); return $this->attachParamsToActions($actions, $params); } @@ -61,7 +63,7 @@ class Action extends Base public function getAll() { $actions = $this->db->table(self::TABLE)->findAll(); - $params = $this->actionParameter->getAll(); + $params = $this->actionParameterModel->getAll(); return $this->attachParamsToActions($actions, $params); } @@ -77,12 +79,24 @@ class Action extends Base $action = $this->db->table(self::TABLE)->eq('id', $action_id)->findOne(); if (! empty($action)) { - $action['params'] = $this->actionParameter->getAllByAction($action_id); + $action['params'] = $this->actionParameterModel->getAllByAction($action_id); } return $action; } + /** + * Get the projectId by the actionId + * + * @access public + * @param integer $action_id + * @return integer + */ + public function getProjectId($action_id) + { + return $this->db->table(self::TABLE)->eq('id', $action_id)->findOneColumn('project_id') ?: 0; + } + /** * Attach parameters to actions * @@ -136,7 +150,7 @@ class Action extends Base $action_id = $this->db->getLastId(); - if (! $this->actionParameter->create($action_id, $values)) { + if (! $this->actionParameterModel->create($action_id, $values)) { $this->db->cancelTransaction(); return false; } @@ -156,7 +170,7 @@ class Action extends Base */ public function duplicate($src_project_id, $dst_project_id) { - $actions = $this->action->getAllByProject($src_project_id); + $actions = $this->actionModel->getAllByProject($src_project_id); foreach ($actions as $action) { $this->db->startTransaction(); @@ -174,7 +188,7 @@ class Action extends Base $action_id = $this->db->getLastId(); - if (! $this->actionParameter->duplicateParameters($dst_project_id, $action_id, $action['params'])) { + if (! $this->actionParameterModel->duplicateParameters($dst_project_id, $action_id, $action['params'])) { $this->logger->error('Action::duplicate => skip action '.$action['action_name'].' '.$action['id']); $this->db->cancelTransaction(); continue; diff --git a/sources/app/Model/ActionParameter.php b/sources/app/Model/ActionParameterModel.php similarity index 87% rename from sources/app/Model/ActionParameter.php rename to sources/app/Model/ActionParameterModel.php index 53edcbc..9895da0 100644 --- a/sources/app/Model/ActionParameter.php +++ b/sources/app/Model/ActionParameterModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Action Parameter Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class ActionParameter extends Base +class ActionParameterModel extends Base { /** * SQL table name @@ -145,16 +147,16 @@ class ActionParameter extends Base case 'project_id': return $value != $project_id ? $value : false; case 'category_id': - return $this->category->getIdByName($project_id, $this->category->getNameById($value)) ?: false; + return $this->categoryModel->getIdByName($project_id, $this->categoryModel->getNameById($value)) ?: false; case 'src_column_id': case 'dest_column_id': case 'dst_column_id': case 'column_id': - $column = $this->column->getById($value); - return empty($column) ? false : $this->column->getColumnIdByTitle($project_id, $column['title']) ?: false; + $column = $this->columnModel->getById($value); + return empty($column) ? false : $this->columnModel->getColumnIdByTitle($project_id, $column['title']) ?: false; case 'user_id': case 'owner_id': - return $this->projectPermission->isAssignable($project_id, $value) ? $value : false; + return $this->projectPermissionModel->isAssignable($project_id, $value) ? $value : false; default: return $value; } diff --git a/sources/app/Model/AvatarFile.php b/sources/app/Model/AvatarFileModel.php similarity index 57% rename from sources/app/Model/AvatarFile.php rename to sources/app/Model/AvatarFileModel.php index 52d0796..6e36d83 100644 --- a/sources/app/Model/AvatarFile.php +++ b/sources/app/Model/AvatarFileModel.php @@ -3,14 +3,15 @@ namespace Kanboard\Model; use Exception; +use Kanboard\Core\Base; /** * Avatar File * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class AvatarFile extends Base +class AvatarFileModel extends Base { /** * Path prefix @@ -28,7 +29,7 @@ class AvatarFile extends Base */ public function getFilename($user_id) { - return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('avatar_path'); + return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->findOneColumn('avatar_path'); } /** @@ -41,7 +42,7 @@ class AvatarFile extends Base */ public function create($user_id, $path) { - $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + $result = $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array( 'avatar_path' => $path, )); @@ -60,30 +61,35 @@ class AvatarFile extends Base public function remove($user_id) { try { - $this->objectStorage->remove($this->getFilename($user_id)); - $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('avatar_path' => '')); - $this->userSession->refresh($user_id); - return $result; + $filename = $this->getFilename($user_id); + + if (! empty($filename)) { + $this->objectStorage->remove($filename); + return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array('avatar_path' => '')); + } } catch (Exception $e) { $this->logger->error($e->getMessage()); return false; } + + return true; } /** - * Upload avatar image + * Upload avatar image file * * @access public * @param integer $user_id * @param array $file + * @return boolean */ - public function uploadFile($user_id, array $file) + public function uploadImageFile($user_id, array $file) { try { if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) { - $destination_filename = $this->generatePath($user_id, $file['name']); - $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename); - $this->create($user_id, $destination_filename); + $destinationFilename = $this->generatePath($user_id, $file['name']); + $this->objectStorage->moveUploadedFile($file['tmp_name'], $destinationFilename); + $this->create($user_id, $destinationFilename); } else { throw new Exception('File not uploaded: '.var_export($file['error'], true)); } @@ -96,6 +102,28 @@ class AvatarFile extends Base return true; } + /** + * Upload avatar image content + * + * @access public + * @param integer $user_id + * @param string $blob + * @return boolean + */ + public function uploadImageContent($user_id, &$blob) + { + try { + $destinationFilename = $this->generatePath($user_id, 'imageContent'); + $this->objectStorage->put($destinationFilename, $blob); + $this->create($user_id, $destinationFilename); + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + return false; + } + + return true; + } + /** * Generate the path for a new filename * diff --git a/sources/app/Model/Base.php b/sources/app/Model/Base.php deleted file mode 100644 index 714b430..0000000 --- a/sources/app/Model/Base.php +++ /dev/null @@ -1,58 +0,0 @@ -db->transaction(function (Database $db) use ($table, $values) { - - if (! $db->table($table)->save($values)) { - return false; - } - - return (int) $db->getLastId(); - }); - } - - /** - * Build SQL condition for a given time range - * - * @access protected - * @param string $start_time Start timestamp - * @param string $end_time End timestamp - * @param string $start_column Start column name - * @param string $end_column End column name - * @return string - */ - protected function getCalendarCondition($start_time, $end_time, $start_column, $end_column) - { - $start_column = $this->db->escapeIdentifier($start_column); - $end_column = $this->db->escapeIdentifier($end_column); - - $conditions = array( - "($start_column >= '$start_time' AND $start_column <= '$end_time')", - "($start_column <= '$start_time' AND $end_column >= '$start_time')", - "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))", - ); - - return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')'; - } -} diff --git a/sources/app/Model/Board.php b/sources/app/Model/BoardModel.php similarity index 51% rename from sources/app/Model/Board.php rename to sources/app/Model/BoardModel.php index d41ecaf..4d55993 100644 --- a/sources/app/Model/Board.php +++ b/sources/app/Model/BoardModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Board model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Board extends Base +class BoardModel extends Base { /** * Get Kanboard default columns @@ -29,7 +31,7 @@ class Board extends Base */ public function getUserColumns() { - $column_names = explode(',', $this->config->get('board_columns', implode(',', $this->getDefaultColumns()))); + $column_names = explode(',', $this->configModel->get('board_columns', implode(',', $this->getDefaultColumns()))); $columns = array(); foreach ($column_names as $column_name) { @@ -64,7 +66,7 @@ class Board extends Base 'description' => $column['description'], ); - if (! $this->db->table(Column::TABLE)->save($values)) { + if (! $this->db->table(ColumnModel::TABLE)->save($values)) { return false; } } @@ -82,73 +84,13 @@ class Board extends Base */ public function duplicate($project_from, $project_to) { - $columns = $this->db->table(Column::TABLE) + $columns = $this->db->table(ColumnModel::TABLE) ->columns('title', 'task_limit', 'description') ->eq('project_id', $project_from) ->asc('position') ->findAll(); - return $this->board->create($project_to, $columns); - } - - /** - * Get all tasks sorted by columns and swimlanes - * - * @access public - * @param integer $project_id - * @param callable $callback - * @return array - */ - public function getBoard($project_id, $callback = null) - { - $swimlanes = $this->swimlane->getSwimlanes($project_id); - $columns = $this->column->getAll($project_id); - $nb_columns = count($columns); - - for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) { - $swimlanes[$i]['columns'] = $columns; - $swimlanes[$i]['nb_columns'] = $nb_columns; - $swimlanes[$i]['nb_tasks'] = 0; - $swimlanes[$i]['nb_swimlanes'] = $ilen; - - for ($j = 0; $j < $nb_columns; $j++) { - $column_id = $columns[$j]['id']; - $swimlane_id = $swimlanes[$i]['id']; - - if (! isset($swimlanes[0]['columns'][$j]['nb_column_tasks'])) { - $swimlanes[0]['columns'][$j]['nb_column_tasks'] = 0; - $swimlanes[0]['columns'][$j]['total_score'] = 0; - } - - $swimlanes[$i]['columns'][$j]['tasks'] = $callback === null ? $this->taskFinder->getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id) : $callback($project_id, $column_id, $swimlane_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']; - $swimlanes[0]['columns'][$j]['nb_column_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks']; - $swimlanes[0]['columns'][$j]['total_score'] += $swimlanes[$i]['columns'][$j]['score']; - } - } - - 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[$field]; - } - - return $sum; + return $this->boardModel->create($project_to, $columns); } /** @@ -162,7 +104,7 @@ class Board extends Base public function getColumnStats($project_id, $prepend = false) { $listing = $this->db - ->hashtable(Task::TABLE) + ->hashtable(TaskModel::TABLE) ->eq('project_id', $project_id) ->eq('is_active', 1) ->groupBy('column_id') diff --git a/sources/app/Model/Category.php b/sources/app/Model/CategoryModel.php similarity index 88% rename from sources/app/Model/Category.php rename to sources/app/Model/CategoryModel.php index 1d5f654..024d002 100644 --- a/sources/app/Model/Category.php +++ b/sources/app/Model/CategoryModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Category model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Category extends Base +class CategoryModel extends Base { /** * SQL table name @@ -53,6 +55,18 @@ class Category extends Base return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('name') ?: ''; } + /** + * Get the projectId by the category id + * + * @access public + * @param integer $category_id Category id + * @return integer + */ + public function getProjectId($category_id) + { + return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('project_id') ?: 0; + } + /** * Get a category id by the category name and project id * @@ -123,7 +137,7 @@ class Category extends Base public function createDefaultCategories($project_id) { $results = array(); - $categories = explode(',', $this->config->get('project_categories')); + $categories = explode(',', $this->configModel->get('project_categories')); foreach ($categories as $category) { $category = trim($category); @@ -148,7 +162,7 @@ class Category extends Base */ public function create(array $values) { - return $this->persist(self::TABLE, $values); + return $this->db->table(self::TABLE)->persist($values); } /** @@ -174,7 +188,7 @@ class Category extends Base { $this->db->startTransaction(); - $this->db->table(Task::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0)); + $this->db->table(TaskModel::TABLE)->eq('category_id', $category_id)->update(array('category_id' => 0)); if (! $this->db->table(self::TABLE)->eq('id', $category_id)->remove()) { $this->db->cancelTransaction(); diff --git a/sources/app/Model/Color.php b/sources/app/Model/ColorModel.php similarity index 97% rename from sources/app/Model/Color.php rename to sources/app/Model/ColorModel.php index dee2864..9e69dda 100644 --- a/sources/app/Model/Color.php +++ b/sources/app/Model/ColorModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Color model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Color extends Base +class ColorModel extends Base { /** * Default colors @@ -163,7 +165,7 @@ class Color extends Base */ public function getDefaultColor() { - return $this->config->get('default_color', 'yellow'); + return $this->configModel->get('default_color', 'yellow'); } /** diff --git a/sources/app/Model/Column.php b/sources/app/Model/ColumnModel.php similarity index 92% rename from sources/app/Model/Column.php rename to sources/app/Model/ColumnModel.php index ccdcb04..795fe69 100644 --- a/sources/app/Model/Column.php +++ b/sources/app/Model/ColumnModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Column Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Column extends Base +class ColumnModel extends Base { /** * SQL table name @@ -29,6 +31,18 @@ class Column extends Base return $this->db->table(self::TABLE)->eq('id', $column_id)->findOne(); } + /** + * Get projectId by the columnId + * + * @access public + * @param integer $column_id Column id + * @return integer + */ + public function getProjectId($column_id) + { + return $this->db->table(self::TABLE)->eq('id', $column_id)->findOneColumn('project_id'); + } + /** * Get the first column id for a given project * @@ -140,7 +154,7 @@ class Column extends Base 'description' => $description, ); - return $this->persist(self::TABLE, $values); + return $this->db->table(self::TABLE)->persist($values); } /** diff --git a/sources/app/Model/Comment.php b/sources/app/Model/CommentModel.php similarity index 73% rename from sources/app/Model/Comment.php rename to sources/app/Model/CommentModel.php index f7ac4ea..4231f29 100644 --- a/sources/app/Model/Comment.php +++ b/sources/app/Model/CommentModel.php @@ -3,14 +3,15 @@ namespace Kanboard\Model; use Kanboard\Event\CommentEvent; +use Kanboard\Core\Base; /** * Comment model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Comment extends Base +class CommentModel extends Base { /** * SQL table name @@ -28,6 +29,22 @@ class Comment extends Base const EVENT_CREATE = 'comment.create'; const EVENT_USER_MENTION = 'comment.user.mention'; + /** + * Get projectId from commentId + * + * @access public + * @param integer $comment_id + * @return integer + */ + public function getProjectId($comment_id) + { + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $comment_id) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; + } + /** * Get all comments for a given task * @@ -46,12 +63,12 @@ class Comment extends Base self::TABLE.'.task_id', self::TABLE.'.user_id', self::TABLE.'.comment', - User::TABLE.'.username', - User::TABLE.'.name', - User::TABLE.'.email', - User::TABLE.'.avatar_path' + UserModel::TABLE.'.username', + UserModel::TABLE.'.name', + UserModel::TABLE.'.email', + UserModel::TABLE.'.avatar_path' ) - ->join(User::TABLE, 'id', 'user_id') + ->join(UserModel::TABLE, 'id', 'user_id') ->orderBy(self::TABLE.'.date_creation', $sorting) ->eq(self::TABLE.'.task_id', $task_id) ->findAll(); @@ -75,10 +92,12 @@ class Comment extends Base self::TABLE.'.date_creation', self::TABLE.'.comment', self::TABLE.'.reference', - User::TABLE.'.username', - User::TABLE.'.name' + UserModel::TABLE.'.username', + UserModel::TABLE.'.name', + UserModel::TABLE.'.email', + UserModel::TABLE.'.avatar_path' ) - ->join(User::TABLE, 'id', 'user_id') + ->join(UserModel::TABLE, 'id', 'user_id') ->eq(self::TABLE.'.id', $comment_id) ->findOne(); } @@ -108,12 +127,12 @@ class Comment extends Base public function create(array $values) { $values['date_creation'] = time(); - $comment_id = $this->persist(self::TABLE, $values); + $comment_id = $this->db->table(self::TABLE)->persist($values); - if ($comment_id) { + if ($comment_id !== false) { $event = new CommentEvent(array('id' => $comment_id) + $values); $this->dispatcher->dispatch(self::EVENT_CREATE, $event); - $this->userMention->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event); + $this->userMentionModel->fireEvents($values['comment'], self::EVENT_USER_MENTION, $event); } return $comment_id; diff --git a/sources/app/Model/Config.php b/sources/app/Model/Config.php deleted file mode 100644 index 0c363fb..0000000 --- a/sources/app/Model/Config.php +++ /dev/null @@ -1,255 +0,0 @@ - t('Application default')) + $listing; - } - - return $listing; - } - - /** - * Get current timezone - * - * @access public - * @return string - */ - public function getCurrentTimezone() - { - if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['timezone'])) { - return $this->sessionStorage->user['timezone']; - } - - return $this->get('application_timezone', 'UTC'); - } - - /** - * Set timezone - * - * @access public - */ - public function setupTimezone() - { - date_default_timezone_set($this->getCurrentTimezone()); - } - - /** - * Get available languages - * - * @access public - * @param boolean $prepend Prepend a default value - * @return array - */ - public function getLanguages($prepend = false) - { - // Sorted by value - $languages = array( - 'id_ID' => 'Bahasa Indonesia', - 'bs_BA' => 'Bosanski', - 'cs_CZ' => 'Čeština', - 'da_DK' => 'Dansk', - 'de_DE' => 'Deutsch', - 'en_US' => 'English', - 'es_ES' => 'Español', - 'fr_FR' => 'Français', - 'el_GR' => 'Grec', - 'it_IT' => 'Italiano', - 'hu_HU' => 'Magyar', - 'my_MY' => 'Melayu', - 'nl_NL' => 'Nederlands', - 'nb_NO' => 'Norsk', - 'pl_PL' => 'Polski', - 'pt_PT' => 'Português', - 'pt_BR' => 'Português (Brasil)', - 'ru_RU' => 'Русский', - 'sr_Latn_RS' => 'Srpski', - 'fi_FI' => 'Suomi', - 'sv_SE' => 'Svenska', - 'tr_TR' => 'Türkçe', - 'ko_KR' => '한국어', - '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( - 'cs_CZ' => 'cs', - 'da_DK' => 'da', - 'de_DE' => 'de', - 'en_US' => 'en', - 'es_ES' => 'es', - 'fr_FR' => 'fr', - 'it_IT' => 'it', - 'hu_HU' => 'hu', - 'nl_NL' => 'nl', - 'nb_NO' => 'nb', - 'pl_PL' => 'pl', - 'pt_PT' => 'pt', - 'pt_BR' => 'pt-br', - 'ru_RU' => 'ru', - 'sr_Latn_RS' => 'sr', - 'fi_FI' => 'fi', - 'sv_SE' => 'sv', - 'tr_TR' => 'tr', - 'ko_KR' => 'ko', - 'zh_CN' => 'zh-cn', - 'ja_JP' => 'ja', - 'th_TH' => 'th', - 'id_ID' => 'id', - 'el_GR' => 'el', - ); - - $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->sessionStorage->user['language'])) { - return $this->sessionStorage->user['language']; - } - - return $this->get('application_language', 'en_US'); - } - - /** - * Load translations - * - * @access public - */ - public function setupTranslations() - { - Translator::load($this->getCurrentLanguage()); - } - - /** - * Get a config variable from the session or the database - * - * @access public - * @param string $name Parameter name - * @param string $default_value Default value of the parameter - * @return string - */ - public function get($name, $default_value = '') - { - $options = $this->memoryCache->proxy($this, 'getAll'); - return isset($options[$name]) && $options[$name] !== '' ? $options[$name] : $default_value; - } - - /** - * Reload settings in the session and the translations - * - * @access public - */ - public function reload() - { - $this->setupTranslations(); - } - - /** - * Optimize the Sqlite database - * - * @access public - * @return boolean - */ - public function optimizeDatabase() - { - return $this->db->getconnection()->exec('VACUUM'); - } - - /** - * Compress the Sqlite database - * - * @access public - * @return string - */ - public function downloadDatabase() - { - return gzencode(file_get_contents(DB_FILENAME)); - } - - /** - * Get the Sqlite database size in bytes - * - * @access public - * @return integer - */ - public function getDatabaseSize() - { - return DB_DRIVER === 'sqlite' ? filesize(DB_FILENAME) : 0; - } - - /** - * Regenerate a token - * - * @access public - * @param string $option Parameter name - * @return boolean - */ - public function regenerateToken($option) - { - return $this->save(array($option => Token::getToken())); - } - - /** - * Prepare data before save - * - * @access public - * @param array $values - * @return array - */ - public function prepare(array $values) - { - if (! empty($values['application_url']) && substr($values['application_url'], -1) !== '/') { - $values['application_url'] = $values['application_url'].'/'; - } - - return $values; - } -} diff --git a/sources/app/Model/ConfigModel.php b/sources/app/Model/ConfigModel.php new file mode 100644 index 0000000..945c5e6 --- /dev/null +++ b/sources/app/Model/ConfigModel.php @@ -0,0 +1,89 @@ +memoryCache->proxy($this, 'getAll'); + return isset($options[$name]) && $options[$name] !== '' ? $options[$name] : $default_value; + } + + /** + * Optimize the Sqlite database + * + * @access public + * @return boolean + */ + public function optimizeDatabase() + { + return $this->db->getConnection()->exec('VACUUM'); + } + + /** + * Compress the Sqlite database + * + * @access public + * @return string + */ + public function downloadDatabase() + { + return gzencode(file_get_contents(DB_FILENAME)); + } + + /** + * Get the Sqlite database size in bytes + * + * @access public + * @return integer + */ + public function getDatabaseSize() + { + return DB_DRIVER === 'sqlite' ? filesize(DB_FILENAME) : 0; + } + + /** + * Regenerate a token + * + * @access public + * @param string $option Parameter name + * @return boolean + */ + public function regenerateToken($option) + { + return $this->save(array($option => Token::getToken())); + } + + /** + * Prepare data before save + * + * @access public + * @param array $values + * @return array + */ + public function prepare(array $values) + { + if (! empty($values['application_url']) && substr($values['application_url'], -1) !== '/') { + $values['application_url'] = $values['application_url'].'/'; + } + + return $values; + } +} diff --git a/sources/app/Model/Currency.php b/sources/app/Model/CurrencyModel.php similarity index 94% rename from sources/app/Model/Currency.php rename to sources/app/Model/CurrencyModel.php index abcce2f..bfd9697 100644 --- a/sources/app/Model/Currency.php +++ b/sources/app/Model/CurrencyModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Currency * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Currency extends Base +class CurrencyModel extends Base { /** * SQL table name @@ -65,7 +67,7 @@ class Currency extends Base public function getPrice($currency, $price) { static $rates = null; - $reference = $this->config->get('application_currency', 'USD'); + $reference = $this->configModel->get('application_currency', 'USD'); if ($reference !== $currency) { $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : $rates; diff --git a/sources/app/Model/CustomFilter.php b/sources/app/Model/CustomFilterModel.php similarity index 86% rename from sources/app/Model/CustomFilter.php rename to sources/app/Model/CustomFilterModel.php index 3a6a1a3..a4c23b5 100644 --- a/sources/app/Model/CustomFilter.php +++ b/sources/app/Model/CustomFilterModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Custom Filter model * - * @package model + * @package Kanboard\Model * @author Timo Litzbarski */ -class CustomFilter extends Base +class CustomFilterModel extends Base { /** * SQL table name @@ -30,8 +32,8 @@ class CustomFilter extends Base return $this->db ->table(self::TABLE) ->columns( - User::TABLE.'.name as owner_name', - User::TABLE.'.username as owner_username', + UserModel::TABLE.'.name as owner_name', + UserModel::TABLE.'.username as owner_username', self::TABLE.'.id', self::TABLE.'.user_id', self::TABLE.'.project_id', @@ -41,7 +43,7 @@ class CustomFilter extends Base self::TABLE.'.append' ) ->asc(self::TABLE.'.name') - ->join(User::TABLE, 'id', 'user_id') + ->join(UserModel::TABLE, 'id', 'user_id') ->beginOr() ->eq('is_shared', 1) ->eq('user_id', $user_id) @@ -71,7 +73,7 @@ class CustomFilter extends Base */ public function create(array $values) { - return $this->persist(self::TABLE, $values); + return $this->db->table(self::TABLE)->persist($values); } /** diff --git a/sources/app/Model/File.php b/sources/app/Model/FileModel.php similarity index 77% rename from sources/app/Model/File.php rename to sources/app/Model/FileModel.php index e383235..8cdea9a 100644 --- a/sources/app/Model/File.php +++ b/sources/app/Model/FileModel.php @@ -3,6 +3,7 @@ namespace Kanboard\Model; use Exception; +use Kanboard\Core\Base; use Kanboard\Core\Thumbnail; use Kanboard\Event\FileEvent; use Kanboard\Core\ObjectStorage\ObjectStorageException; @@ -10,11 +11,47 @@ use Kanboard\Core\ObjectStorage\ObjectStorageException; /** * Base File Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -abstract class File extends Base +abstract class FileModel extends Base { + /** + * Get the table + * + * @abstract + * @access protected + * @return string + */ + abstract protected function getTable(); + + /** + * Define the foreign key + * + * @abstract + * @access protected + * @return string + */ + abstract protected function getForeignKey(); + + /** + * Get the path prefix + * + * @abstract + * @access protected + * @return string + */ + abstract protected function getPathPrefix(); + + /** + * Get event name + * + * @abstract + * @access protected + * @return string + */ + abstract protected function getEventName(); + /** * Get PicoDb query to get all files * @@ -24,21 +61,21 @@ abstract class File extends Base protected function getQuery() { return $this->db - ->table(static::TABLE) + ->table($this->getTable()) ->columns( - static::TABLE.'.id', - static::TABLE.'.name', - static::TABLE.'.path', - static::TABLE.'.is_image', - static::TABLE.'.'.static::FOREIGN_KEY, - static::TABLE.'.date', - static::TABLE.'.user_id', - static::TABLE.'.size', - User::TABLE.'.username', - User::TABLE.'.name as user_name' + $this->getTable().'.id', + $this->getTable().'.name', + $this->getTable().'.path', + $this->getTable().'.is_image', + $this->getTable().'.'.$this->getForeignKey(), + $this->getTable().'.date', + $this->getTable().'.user_id', + $this->getTable().'.size', + UserModel::TABLE.'.username', + UserModel::TABLE.'.name as user_name' ) - ->join(User::TABLE, 'id', 'user_id') - ->asc(static::TABLE.'.name'); + ->join(UserModel::TABLE, 'id', 'user_id') + ->asc($this->getTable().'.name'); } /** @@ -50,7 +87,7 @@ abstract class File extends Base */ public function getById($file_id) { - return $this->db->table(static::TABLE)->eq('id', $file_id)->findOne(); + return $this->db->table($this->getTable())->eq('id', $file_id)->findOne(); } /** @@ -62,7 +99,7 @@ abstract class File extends Base */ public function getAll($id) { - return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->findAll(); + return $this->getQuery()->eq($this->getForeignKey(), $id)->findAll(); } /** @@ -74,7 +111,7 @@ abstract class File extends Base */ public function getAllImages($id) { - return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 1)->findAll(); + return $this->getQuery()->eq($this->getForeignKey(), $id)->eq('is_image', 1)->findAll(); } /** @@ -86,7 +123,7 @@ abstract class File extends Base */ public function getAllDocuments($id) { - return $this->getQuery()->eq(static::FOREIGN_KEY, $id)->eq('is_image', 0)->findAll(); + return $this->getQuery()->eq($this->getForeignKey(), $id)->eq('is_image', 0)->findAll(); } /** @@ -102,7 +139,7 @@ abstract class File extends Base public function create($id, $name, $path, $size) { $values = array( - static::FOREIGN_KEY => $id, + $this->getForeignKey() => $id, 'name' => substr($name, 0, 255), 'path' => $path, 'is_image' => $this->isImage($name) ? 1 : 0, @@ -111,12 +148,12 @@ abstract class File extends Base 'date' => time(), ); - $result = $this->db->table(static::TABLE)->insert($values); + $result = $this->db->table($this->getTable())->insert($values); if ($result) { $file_id = (int) $this->db->getLastId(); $event = new FileEvent($values + array('file_id' => $file_id)); - $this->dispatcher->dispatch(static::EVENT_CREATE, $event); + $this->dispatcher->dispatch($this->getEventName(), $event); return $file_id; } @@ -132,7 +169,7 @@ abstract class File extends Base */ public function removeAll($id) { - $file_ids = $this->db->table(static::TABLE)->eq(static::FOREIGN_KEY, $id)->asc('id')->findAllByColumn('id'); + $file_ids = $this->db->table($this->getTable())->eq($this->getForeignKey(), $id)->asc('id')->findAllByColumn('id'); $results = array(); foreach ($file_ids as $file_id) { @@ -159,7 +196,7 @@ abstract class File extends Base $this->objectStorage->remove($this->getThumbnailPath($file['path'])); } - return $this->db->table(static::TABLE)->eq('id', $file['id'])->remove(); + return $this->db->table($this->getTable())->eq('id', $file['id'])->remove(); } catch (ObjectStorageException $e) { $this->logger->error($e->getMessage()); return false; @@ -210,7 +247,7 @@ abstract class File extends Base */ public function generatePath($id, $filename) { - return static::PATH_PREFIX.DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time()); + return $this->getPathPrefix().DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time()); } /** @@ -252,6 +289,7 @@ abstract class File extends Base * @access public * @param integer $id * @param array $file + * @throws Exception */ public function uploadFile($id, array $file) { diff --git a/sources/app/Model/GroupMember.php b/sources/app/Model/GroupMemberModel.php similarity index 75% rename from sources/app/Model/GroupMember.php rename to sources/app/Model/GroupMemberModel.php index 7ed5f73..a207778 100644 --- a/sources/app/Model/GroupMember.php +++ b/sources/app/Model/GroupMemberModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Group Member Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class GroupMember extends Base +class GroupMemberModel extends Base { /** * SQL table name @@ -27,7 +29,7 @@ class GroupMember extends Base public function getQuery($group_id) { return $this->db->table(self::TABLE) - ->join(User::TABLE, 'id', 'user_id') + ->join(UserModel::TABLE, 'id', 'user_id') ->eq('group_id', $group_id); } @@ -56,7 +58,7 @@ class GroupMember extends Base ->columns('user_id') ->eq('group_id', $group_id); - return $this->db->table(User::TABLE) + return $this->db->table(UserModel::TABLE) ->notInSubquery('id', $subquery) ->findAll(); } @@ -108,4 +110,21 @@ class GroupMember extends Base ->eq('user_id', $user_id) ->exists(); } + + /** + * Get all groups for a given user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getGroups($user_id) + { + return $this->db->table(self::TABLE) + ->columns(GroupModel::TABLE.'.id', GroupModel::TABLE.'.external_id', GroupModel::TABLE.'.name') + ->join(GroupModel::TABLE, 'id', 'group_id') + ->eq(self::TABLE.'.user_id', $user_id) + ->asc(GroupModel::TABLE.'.name') + ->findAll(); + } } diff --git a/sources/app/Model/Group.php b/sources/app/Model/GroupModel.php similarity index 92% rename from sources/app/Model/Group.php rename to sources/app/Model/GroupModel.php index 6789950..0a97557 100644 --- a/sources/app/Model/Group.php +++ b/sources/app/Model/GroupModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Group Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Group extends Base +class GroupModel extends Base { /** * SQL table name @@ -80,7 +82,7 @@ class Group extends Base * * @access public * @param integer $group_id - * @return array + * @return boolean */ public function remove($group_id) { @@ -97,7 +99,7 @@ class Group extends Base */ public function create($name, $external_id = '') { - return $this->persist(self::TABLE, array( + return $this->db->table(self::TABLE)->persist(array( 'name' => $name, 'external_id' => $external_id, )); diff --git a/sources/app/Model/LanguageModel.php b/sources/app/Model/LanguageModel.php new file mode 100644 index 0000000..eb6de00 --- /dev/null +++ b/sources/app/Model/LanguageModel.php @@ -0,0 +1,179 @@ + 'Bahasa Indonesia', + 'bs_BA' => 'Bosanski', + 'cs_CZ' => 'Čeština', + 'da_DK' => 'Dansk', + 'de_DE' => 'Deutsch', + 'en_US' => 'English', + 'es_ES' => 'Español', + 'fr_FR' => 'Français', + 'el_GR' => 'Grec', + 'it_IT' => 'Italiano', + 'hu_HU' => 'Magyar', + 'my_MY' => 'Melayu', + 'nl_NL' => 'Nederlands', + 'nb_NO' => 'Norsk', + 'pl_PL' => 'Polski', + 'pt_PT' => 'Português', + 'pt_BR' => 'Português (Brasil)', + 'ru_RU' => 'Русский', + 'sr_Latn_RS' => 'Srpski', + 'fi_FI' => 'Suomi', + 'sv_SE' => 'Svenska', + 'tr_TR' => 'Türkçe', + 'ko_KR' => '한국어', + '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( + 'cs_CZ' => 'cs', + 'da_DK' => 'da', + 'de_DE' => 'de', + 'en_US' => 'en', + 'es_ES' => 'es', + 'fr_FR' => 'fr', + 'it_IT' => 'it', + 'hu_HU' => 'hu', + 'nl_NL' => 'nl', + 'nb_NO' => 'nb', + 'pl_PL' => 'pl', + 'pt_PT' => 'pt', + 'pt_BR' => 'pt-br', + 'ru_RU' => 'ru', + 'sr_Latn_RS' => 'sr', + 'fi_FI' => 'fi', + 'sv_SE' => 'sv', + 'tr_TR' => 'tr', + 'ko_KR' => 'ko', + 'zh_CN' => 'zh-cn', + 'ja_JP' => 'ja', + 'th_TH' => 'th', + 'id_ID' => 'id', + 'el_GR' => 'el', + ); + + $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->sessionStorage->user['language'])) { + return $this->sessionStorage->user['language']; + } + + return $this->configModel->get('application_language', 'en_US'); + } + + /** + * Load translations for the current language + * + * @access public + */ + public function loadCurrentLanguage() + { + Translator::load($this->getCurrentLanguage()); + } +} diff --git a/sources/app/Model/LastLogin.php b/sources/app/Model/LastLoginModel.php similarity index 93% rename from sources/app/Model/LastLogin.php rename to sources/app/Model/LastLoginModel.php index feb5f5a..caaa4a8 100644 --- a/sources/app/Model/LastLogin.php +++ b/sources/app/Model/LastLoginModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * LastLogin model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class LastLogin extends Base +class LastLoginModel extends Base { /** * SQL table name @@ -66,7 +68,7 @@ class LastLogin extends Base if (count($connections) >= self::NB_LOGINS) { $this->db->table(self::TABLE) ->eq('user_id', $user_id) - ->notin('id', array_slice($connections, 0, self::NB_LOGINS - 1)) + ->notIn('id', array_slice($connections, 0, self::NB_LOGINS - 1)) ->remove(); } } diff --git a/sources/app/Model/Link.php b/sources/app/Model/LinkModel.php similarity index 98% rename from sources/app/Model/Link.php rename to sources/app/Model/LinkModel.php index 903a98d..b72c753 100644 --- a/sources/app/Model/Link.php +++ b/sources/app/Model/LinkModel.php @@ -3,15 +3,16 @@ namespace Kanboard\Model; use PDO; +use Kanboard\Core\Base; /** * Link model * - * @package model + * @package Kanboard\Model * @author Olivier Maridat * @author Frederic Guillot */ -class Link extends Base +class LinkModel extends Base { /** * SQL table name diff --git a/sources/app/Model/Metadata.php b/sources/app/Model/MetadataModel.php similarity index 80% rename from sources/app/Model/Metadata.php rename to sources/app/Model/MetadataModel.php index 01799a4..6177e5f 100644 --- a/sources/app/Model/Metadata.php +++ b/sources/app/Model/MetadataModel.php @@ -2,14 +2,25 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Metadata * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -abstract class Metadata extends Base +abstract class MetadataModel extends Base { + /** + * Get the table + * + * @abstract + * @access protected + * @return string + */ + abstract protected function getTable(); + /** * Define the entity key * @@ -29,7 +40,7 @@ abstract class Metadata extends Base public function getAll($entity_id) { return $this->db - ->hashtable(static::TABLE) + ->hashtable($this->getTable()) ->eq($this->getEntityKey(), $entity_id) ->asc('name') ->getAll('name', 'value'); @@ -47,7 +58,7 @@ abstract class Metadata extends Base public function get($entity_id, $name, $default = '') { return $this->db - ->table(static::TABLE) + ->table($this->getTable()) ->eq($this->getEntityKey(), $entity_id) ->eq('name', $name) ->findOneColumn('value') ?: $default; @@ -64,7 +75,7 @@ abstract class Metadata extends Base public function exists($entity_id, $name) { return $this->db - ->table(static::TABLE) + ->table($this->getTable()) ->eq($this->getEntityKey(), $entity_id) ->eq('name', $name) ->exists(); @@ -88,7 +99,7 @@ abstract class Metadata extends Base foreach ($values as $key => $value) { if ($this->exists($entity_id, $key)) { - $results[] = $this->db->table(static::TABLE) + $results[] = $this->db->table($this->getTable()) ->eq($this->getEntityKey(), $entity_id) ->eq('name', $key)->update(array( 'value' => $value, @@ -96,7 +107,7 @@ abstract class Metadata extends Base 'changed_by' => $user_id, )); } else { - $results[] = $this->db->table(static::TABLE)->insert(array( + $results[] = $this->db->table($this->getTable())->insert(array( 'name' => $key, 'value' => $value, $this->getEntityKey() => $entity_id, @@ -120,6 +131,9 @@ abstract class Metadata extends Base */ public function remove($entity_id, $name) { - return $this->db->table(static::TABLE)->eq($this->getEntityKey(), $entity_id)->eq('name', $name)->remove(); + return $this->db->table($this->getTable()) + ->eq($this->getEntityKey(), $entity_id) + ->eq('name', $name) + ->remove(); } } diff --git a/sources/app/Model/Notification.php b/sources/app/Model/NotificationModel.php similarity index 63% rename from sources/app/Model/Notification.php rename to sources/app/Model/NotificationModel.php index c252aa3..4d697b5 100644 --- a/sources/app/Model/Notification.php +++ b/sources/app/Model/NotificationModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Notification * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Notification extends Base +class NotificationModel extends Base { /** * Get the event title with author @@ -22,7 +24,7 @@ class Notification extends Base public function getTitleWithAuthor($event_author, $event_name, array $event_data) { switch ($event_name) { - case Task::EVENT_ASSIGNEE_CHANGE: + case TaskModel::EVENT_ASSIGNEE_CHANGE: $assignee = $event_data['task']['assignee_name'] ?: $event_data['task']['assignee_username']; if (! empty($assignee)) { @@ -30,22 +32,22 @@ class Notification extends Base } return e('%s remove the assignee of the task %s', $event_author, e('#%d', $event_data['task']['id'])); - case Task::EVENT_UPDATE: + case TaskModel::EVENT_UPDATE: return e('%s updated the task #%d', $event_author, $event_data['task']['id']); - case Task::EVENT_CREATE: + case TaskModel::EVENT_CREATE: return e('%s created the task #%d', $event_author, $event_data['task']['id']); - case Task::EVENT_CLOSE: + case TaskModel::EVENT_CLOSE: return e('%s closed the task #%d', $event_author, $event_data['task']['id']); - case Task::EVENT_OPEN: + case TaskModel::EVENT_OPEN: return e('%s open the task #%d', $event_author, $event_data['task']['id']); - case Task::EVENT_MOVE_COLUMN: + case TaskModel::EVENT_MOVE_COLUMN: return e( '%s moved the task #%d to the column "%s"', $event_author, $event_data['task']['id'], $event_data['task']['column_title'] ); - case Task::EVENT_MOVE_POSITION: + case TaskModel::EVENT_MOVE_POSITION: return e( '%s moved the task #%d to the position %d in the column "%s"', $event_author, @@ -53,7 +55,7 @@ class Notification extends Base $event_data['task']['position'], $event_data['task']['column_title'] ); - case Task::EVENT_MOVE_SWIMLANE: + case TaskModel::EVENT_MOVE_SWIMLANE: if ($event_data['task']['swimlane_id'] == 0) { return e('%s moved the task #%d to the first swimlane', $event_author, $event_data['task']['id']); } @@ -64,19 +66,19 @@ class Notification extends Base $event_data['task']['id'], $event_data['task']['swimlane_name'] ); - case Subtask::EVENT_UPDATE: + case SubtaskModel::EVENT_UPDATE: return e('%s updated a subtask for the task #%d', $event_author, $event_data['task']['id']); - case Subtask::EVENT_CREATE: + case SubtaskModel::EVENT_CREATE: return e('%s created a subtask for the task #%d', $event_author, $event_data['task']['id']); - case Comment::EVENT_UPDATE: + case CommentModel::EVENT_UPDATE: return e('%s updated a comment on the task #%d', $event_author, $event_data['task']['id']); - case Comment::EVENT_CREATE: + case CommentModel::EVENT_CREATE: return e('%s commented on the task #%d', $event_author, $event_data['task']['id']); - case TaskFile::EVENT_CREATE: + case TaskFileModel::EVENT_CREATE: return e('%s attached a file to the task #%d', $event_author, $event_data['task']['id']); - case Task::EVENT_USER_MENTION: + case TaskModel::EVENT_USER_MENTION: return e('%s mentioned you in the task #%d', $event_author, $event_data['task']['id']); - case Comment::EVENT_USER_MENTION: + case CommentModel::EVENT_USER_MENTION: return e('%s mentioned you in a comment on the task #%d', $event_author, $event_data['task']['id']); default: return e('Notification'); @@ -94,41 +96,78 @@ class Notification extends Base public function getTitleWithoutAuthor($event_name, array $event_data) { switch ($event_name) { - case TaskFile::EVENT_CREATE: + case TaskFileModel::EVENT_CREATE: return e('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); - case Comment::EVENT_CREATE: + case CommentModel::EVENT_CREATE: return e('New comment on task #%d', $event_data['comment']['task_id']); - case Comment::EVENT_UPDATE: + case CommentModel::EVENT_UPDATE: return e('Comment updated on task #%d', $event_data['comment']['task_id']); - case Subtask::EVENT_CREATE: + case SubtaskModel::EVENT_CREATE: return e('New subtask on task #%d', $event_data['subtask']['task_id']); - case Subtask::EVENT_UPDATE: + case SubtaskModel::EVENT_UPDATE: return e('Subtask updated on task #%d', $event_data['subtask']['task_id']); - case Task::EVENT_CREATE: + case TaskModel::EVENT_CREATE: return e('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); - case Task::EVENT_UPDATE: + case TaskModel::EVENT_UPDATE: return e('Task updated #%d', $event_data['task']['id']); - case Task::EVENT_CLOSE: + case TaskModel::EVENT_CLOSE: return e('Task #%d closed', $event_data['task']['id']); - case Task::EVENT_OPEN: + case TaskModel::EVENT_OPEN: return e('Task #%d opened', $event_data['task']['id']); - case Task::EVENT_MOVE_COLUMN: + case TaskModel::EVENT_MOVE_COLUMN: return e('Column changed for task #%d', $event_data['task']['id']); - case Task::EVENT_MOVE_POSITION: + case TaskModel::EVENT_MOVE_POSITION: return e('New position for task #%d', $event_data['task']['id']); - case Task::EVENT_MOVE_SWIMLANE: + case TaskModel::EVENT_MOVE_SWIMLANE: return e('Swimlane changed for task #%d', $event_data['task']['id']); - case Task::EVENT_ASSIGNEE_CHANGE: + case TaskModel::EVENT_ASSIGNEE_CHANGE: return e('Assignee changed on task #%d', $event_data['task']['id']); - case Task::EVENT_OVERDUE: + case TaskModel::EVENT_OVERDUE: $nb = count($event_data['tasks']); return $nb > 1 ? e('%d overdue tasks', $nb) : e('Task #%d is overdue', $event_data['tasks'][0]['id']); - case Task::EVENT_USER_MENTION: + case TaskModel::EVENT_USER_MENTION: return e('You were mentioned in the task #%d', $event_data['task']['id']); - case Comment::EVENT_USER_MENTION: + case CommentModel::EVENT_USER_MENTION: return e('You were mentioned in a comment on the task #%d', $event_data['task']['id']); default: return e('Notification'); } } + + /** + * Get task id from event + * + * @access public + * @param string $event_name + * @param array $event_data + * @return integer + */ + public function getTaskIdFromEvent($event_name, array $event_data) + { + switch ($event_name) { + case TaskFileModel::EVENT_CREATE: + return $event_data['file']['task_id']; + case CommentModel::EVENT_CREATE: + case CommentModel::EVENT_UPDATE: + return $event_data['comment']['task_id']; + case SubtaskModel::EVENT_CREATE: + case SubtaskModel::EVENT_UPDATE: + return $event_data['subtask']['task_id']; + case TaskModel::EVENT_CREATE: + case TaskModel::EVENT_UPDATE: + case TaskModel::EVENT_CLOSE: + case TaskModel::EVENT_OPEN: + case TaskModel::EVENT_MOVE_COLUMN: + case TaskModel::EVENT_MOVE_POSITION: + case TaskModel::EVENT_MOVE_SWIMLANE: + case TaskModel::EVENT_ASSIGNEE_CHANGE: + case CommentModel::EVENT_USER_MENTION: + case TaskModel::EVENT_USER_MENTION: + return $event_data['task']['id']; + case TaskModel::EVENT_OVERDUE: + return $event_data['tasks'][0]['id']; + default: + return 0; + } + } } diff --git a/sources/app/Model/NotificationType.php b/sources/app/Model/NotificationTypeModel.php similarity index 92% rename from sources/app/Model/NotificationType.php rename to sources/app/Model/NotificationTypeModel.php index 289aae9..432832e 100644 --- a/sources/app/Model/NotificationType.php +++ b/sources/app/Model/NotificationTypeModel.php @@ -3,6 +3,7 @@ namespace Kanboard\Model; use Pimple\Container; +use Kanboard\Core\Base; /** * Notification Type @@ -10,7 +11,7 @@ use Pimple\Container; * @package model * @author Frederic Guillot */ -abstract class NotificationType extends Base +abstract class NotificationTypeModel extends Base { /** * Container @@ -56,7 +57,7 @@ abstract class NotificationType extends Base * @param string $label * @param string $class * @param boolean $hidden - * @return NotificationType + * @return NotificationTypeModel */ public function setType($type, $label, $class, $hidden = false) { @@ -80,7 +81,7 @@ abstract class NotificationType extends Base * * @access public * @param string $type - * @return \Kanboard\Notification\NotificationInterface + * @return \Kanboard\Core\Notification\NotificationInterface */ public function getType($type) { diff --git a/sources/app/Model/PasswordReset.php b/sources/app/Model/PasswordResetModel.php similarity index 89% rename from sources/app/Model/PasswordReset.php rename to sources/app/Model/PasswordResetModel.php index 5cfd3c9..d7c7496 100644 --- a/sources/app/Model/PasswordReset.php +++ b/sources/app/Model/PasswordResetModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Password Reset Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class PasswordReset extends Base +class PasswordResetModel extends Base { /** * SQL table name @@ -46,7 +48,7 @@ class PasswordReset extends Base */ public function create($username, $expiration = 0) { - $user_id = $this->db->table(User::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id'); + $user_id = $this->db->table(UserModel::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id'); if (! $user_id) { return false; diff --git a/sources/app/Model/ProjectActivity.php b/sources/app/Model/ProjectActivity.php deleted file mode 100644 index d399d5c..0000000 --- a/sources/app/Model/ProjectActivity.php +++ /dev/null @@ -1,211 +0,0 @@ - $project_id, - 'task_id' => $task_id, - 'creator_id' => $creator_id, - 'event_name' => $event_name, - 'date_creation' => time(), - 'data' => json_encode($data), - ); - - $this->cleanup(self::MAX_EVENTS - 1); - return $this->db->table(self::TABLE)->insert($values); - } - - /** - * Get all events for the given project - * - * @access public - * @param integer $project_id Project id - * @param integer $limit Maximum events number - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - public function getProject($project_id, $limit = 50, $start = null, $end = null) - { - return $this->getProjects(array($project_id), $limit, $start, $end); - } - - /** - * Get all events for the given projects list - * - * @access public - * @param integer[] $project_ids Projects id - * @param integer $limit Maximum events number - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - public function getProjects(array $project_ids, $limit = 50, $start = null, $end = null) - { - if (empty($project_ids)) { - return array(); - } - - $query = $this - ->db - ->table(self::TABLE) - ->columns( - self::TABLE.'.*', - User::TABLE.'.username AS author_username', - User::TABLE.'.name AS author_name', - User::TABLE.'.email', - User::TABLE.'.avatar_path' - ) - ->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', - User::TABLE.'.avatar_path' - ) - ->eq('task_id', $task_id) - ->join(User::TABLE, 'id', 'creator_id') - ->desc(self::TABLE.'.id') - ->limit($limit); - - return $this->getEvents($query, $start, $end); - } - - /** - * Common function to return events - * - * @access public - * @param \PicoDb\Table $query PicoDb Query - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - private function getEvents(\PicoDb\Table $query, $start, $end) - { - if (! is_null($start)) { - $query->gte('date_creation', $start); - } - - if (! is_null($end)) { - $query->lte('date_creation', $end); - } - - $events = $query->findAll(); - - foreach ($events as &$event) { - $event += $this->decode($event['data']); - unset($event['data']); - - $event['author'] = $event['author_name'] ?: $event['author_username']; - $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event); - $event['event_content'] = $this->getContent($event); - } - - return $events; - } - - /** - * Remove old event entries to avoid large table - * - * @access public - * @param integer $max Maximum number of items to keep in the table - */ - public function cleanup($max) - { - $total = $this->db->table(self::TABLE)->count(); - - if ($total > $max) { - $ids = $this->db->table(self::TABLE)->asc('id')->limit($total - $max)->findAllByColumn('id'); - $this->db->table(self::TABLE)->in('id', $ids)->remove(); - } - } - - /** - * Get the event html content - * - * @access public - * @param array $params Event properties - * @return string - */ - public function getContent(array $params) - { - return $this->template->render( - 'event/'.str_replace('.', '_', $params['event_name']), - $params - ); - } - - /** - * Decode event data, supports unserialize() and json_decode() - * - * @access public - * @param string $data Serialized data - * @return array - */ - public function decode($data) - { - if ($data{0} === 'a') { - return unserialize($data); - } - - return json_decode($data, true) ?: array(); - } -} diff --git a/sources/app/Model/ProjectActivityModel.php b/sources/app/Model/ProjectActivityModel.php new file mode 100644 index 0000000..380ea12 --- /dev/null +++ b/sources/app/Model/ProjectActivityModel.php @@ -0,0 +1,94 @@ + $project_id, + 'task_id' => $task_id, + 'creator_id' => $creator_id, + 'event_name' => $event_name, + 'date_creation' => time(), + 'data' => json_encode($data), + ); + + $this->cleanup(self::MAX_EVENTS - 1); + return $this->db->table(self::TABLE)->insert($values); + } + + /** + * Get query + * + * @access public + * @return Table + */ + public function getQuery() + { + return $this + ->db + ->table(ProjectActivityModel::TABLE) + ->columns( + ProjectActivityModel::TABLE.'.*', + 'uc.username AS author_username', + 'uc.name AS author_name', + 'uc.email', + 'uc.avatar_path' + ) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->join(ProjectModel::TABLE, 'id', 'project_id') + ->left(UserModel::TABLE, 'uc', 'id', ProjectActivityModel::TABLE, 'creator_id'); + } + + /** + * Remove old event entries to avoid large table + * + * @access public + * @param integer $max Maximum number of items to keep in the table + */ + public function cleanup($max) + { + $total = $this->db->table(self::TABLE)->count(); + + if ($total > $max) { + $ids = $this->db->table(self::TABLE)->asc('id')->limit($total - $max)->findAllByColumn('id'); + $this->db->table(self::TABLE)->in('id', $ids)->remove(); + } + } +} diff --git a/sources/app/Model/ProjectDailyColumnStats.php b/sources/app/Model/ProjectDailyColumnStatsModel.php similarity index 92% rename from sources/app/Model/ProjectDailyColumnStats.php rename to sources/app/Model/ProjectDailyColumnStatsModel.php index 0706a11..a0f14cf 100644 --- a/sources/app/Model/ProjectDailyColumnStats.php +++ b/sources/app/Model/ProjectDailyColumnStatsModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Project Daily Column Stats * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class ProjectDailyColumnStats extends Base +class ProjectDailyColumnStatsModel extends Base { /** * SQL table name @@ -84,7 +86,7 @@ class ProjectDailyColumnStats extends Base */ public function getAggregatedMetrics($project_id, $from, $to, $field = 'total') { - $columns = $this->column->getList($project_id); + $columns = $this->columnModel->getList($project_id); $metrics = $this->getMetrics($project_id, $from, $to); return $this->buildAggregate($metrics, $columns, $field); } @@ -205,10 +207,10 @@ class ProjectDailyColumnStats extends Base */ private function getScoreByColumns($project_id) { - $stats = $this->db->table(Task::TABLE) + $stats = $this->db->table(TaskModel::TABLE) ->columns('column_id', 'SUM(score) AS score') ->eq('project_id', $project_id) - ->eq('is_active', Task::STATUS_OPEN) + ->eq('is_active', TaskModel::STATUS_OPEN) ->notNull('score') ->groupBy('column_id') ->findAll(); @@ -225,7 +227,7 @@ class ProjectDailyColumnStats extends Base */ private function getTotalByColumns($project_id) { - $stats = $this->db->table(Task::TABLE) + $stats = $this->db->table(TaskModel::TABLE) ->columns('column_id', 'COUNT(*) AS total') ->eq('project_id', $project_id) ->in('is_active', $this->getTaskStatusConfig()) @@ -243,10 +245,10 @@ class ProjectDailyColumnStats extends Base */ private function getTaskStatusConfig() { - if ($this->config->get('cfd_include_closed_tasks') == 1) { - return array(Task::STATUS_OPEN, Task::STATUS_CLOSED); + if ($this->configModel->get('cfd_include_closed_tasks') == 1) { + return array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED); } - return array(Task::STATUS_OPEN); + return array(TaskModel::STATUS_OPEN); } } diff --git a/sources/app/Model/ProjectDailyStats.php b/sources/app/Model/ProjectDailyStatsModel.php similarity index 95% rename from sources/app/Model/ProjectDailyStats.php rename to sources/app/Model/ProjectDailyStatsModel.php index 974f581..0754d26 100644 --- a/sources/app/Model/ProjectDailyStats.php +++ b/sources/app/Model/ProjectDailyStatsModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Project Daily Stats * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class ProjectDailyStats extends Base +class ProjectDailyStatsModel extends Base { /** * SQL table name diff --git a/sources/app/Model/ProjectDuplication.php b/sources/app/Model/ProjectDuplicationModel.php similarity index 74% rename from sources/app/Model/ProjectDuplication.php rename to sources/app/Model/ProjectDuplicationModel.php index 9c5f80a..94b83c8 100644 --- a/sources/app/Model/ProjectDuplication.php +++ b/sources/app/Model/ProjectDuplicationModel.php @@ -2,16 +2,17 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Core\Security\Role; /** * Project Duplication * - * @package model + * @package Kanboard\Model * @author Frederic Guillot * @author Antonio Rabelo */ -class ProjectDuplication extends Base +class ProjectDuplicationModel extends Base { /** * Get list of optional models to duplicate @@ -21,7 +22,15 @@ class ProjectDuplication extends Base */ public function getOptionalSelection() { - return array('category', 'projectPermission', 'action', 'swimlane', 'task'); + return array( + 'categoryModel', + 'projectPermissionModel', + 'actionModel', + 'swimlaneModel', + 'tagDuplicationModel', + 'projectMetadataModel', + 'projectTaskDuplicationModel', + ); } /** @@ -32,7 +41,16 @@ class ProjectDuplication extends Base */ public function getPossibleSelection() { - return array('board', 'category', 'projectPermission', 'action', 'swimlane', 'task'); + return array( + 'boardModel', + 'categoryModel', + 'projectPermissionModel', + 'actionModel', + 'swimlaneModel', + 'tagDuplicationModel', + 'projectMetadataModel', + 'projectTaskDuplicationModel', + ); } /** @@ -64,7 +82,7 @@ class ProjectDuplication extends Base * @param boolean $private Force the project to be private * @return integer Cloned Project Id */ - public function duplicate($src_project_id, $selection = array('projectPermission', 'category', 'action'), $owner_id = 0, $name = null, $private = null) + public function duplicate($src_project_id, $selection = array('projectPermissionModel', 'categoryModel', 'actionModel'), $owner_id = 0, $name = null, $private = null) { $this->db->startTransaction(); @@ -85,7 +103,7 @@ class ProjectDuplication extends Base } // Skip permissions for private projects - if ($private && $model === 'projectPermission') { + if ($private && $model === 'projectPermissionModel') { continue; } @@ -117,7 +135,7 @@ class ProjectDuplication extends Base */ private function copy($src_project_id, $owner_id = 0, $name = null, $private = null) { - $project = $this->project->getById($src_project_id); + $project = $this->projectModel->getById($src_project_id); $is_private = empty($project['is_private']) ? 0 : 1; $values = array( @@ -128,9 +146,12 @@ class ProjectDuplication extends Base 'is_public' => 0, 'is_private' => $private ? 1 : $is_private, 'owner_id' => $owner_id, + 'priority_default' => $project['priority_default'], + 'priority_start' => $project['priority_start'], + 'priority_end' => $project['priority_end'], ); - if (! $this->db->table(Project::TABLE)->save($values)) { + if (! $this->db->table(ProjectModel::TABLE)->save($values)) { return false; } @@ -148,9 +169,9 @@ class ProjectDuplication extends Base private function makeOwnerManager($dst_project_id, $owner_id) { if ($owner_id > 0) { - $this->projectUserRole->removeUser($dst_project_id, $owner_id); + $this->projectUserRoleModel->removeUser($dst_project_id, $owner_id); - if (! $this->projectUserRole->addUser($dst_project_id, $owner_id, Role::PROJECT_MANAGER)) { + if (! $this->projectUserRoleModel->addUser($dst_project_id, $owner_id, Role::PROJECT_MANAGER)) { return false; } } diff --git a/sources/app/Model/ProjectFile.php b/sources/app/Model/ProjectFile.php deleted file mode 100644 index aa9bf15..0000000 --- a/sources/app/Model/ProjectFile.php +++ /dev/null @@ -1,40 +0,0 @@ -query = $this->db->table(ProjectGroupRole::TABLE); - return $this; - } - - /** - * Get all results of the filter - * - * @access public - * @param string $column - * @return array - */ - public function findAll($column = '') - { - if ($column !== '') { - return $this->query->asc($column)->findAllByColumn($column); - } - - return $this->query->findAll(); - } - - /** - * Get the PicoDb query - * - * @access public - * @return \PicoDb\Table - */ - public function getQuery() - { - return $this->query; - } - - /** - * Filter by project id - * - * @access public - * @param integer $project_id - * @return ProjectUserRoleFilter - */ - public function filterByProjectId($project_id) - { - $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id); - return $this; - } - - /** - * Filter by username - * - * @access public - * @param string $input - * @return ProjectUserRoleFilter - */ - public function startWithUsername($input) - { - $this->query - ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) - ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) - ->ilike(User::TABLE.'.username', $input.'%'); - - return $this; - } -} diff --git a/sources/app/Model/ProjectGroupRole.php b/sources/app/Model/ProjectGroupRoleModel.php similarity index 74% rename from sources/app/Model/ProjectGroupRole.php rename to sources/app/Model/ProjectGroupRoleModel.php index afad4a4..2729d5a 100644 --- a/sources/app/Model/ProjectGroupRole.php +++ b/sources/app/Model/ProjectGroupRoleModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Core\Security\Role; /** * Project Group Role * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class ProjectGroupRole extends Base +class ProjectGroupRoleModel extends Base { /** * SQL table name @@ -27,15 +28,15 @@ class ProjectGroupRole extends Base * @param array $status * @return array */ - public function getProjectsByUser($user_id, $status = array(Project::ACTIVE, Project::INACTIVE)) + public function getProjectsByUser($user_id, $status = array(ProjectModel::ACTIVE, ProjectModel::INACTIVE)) { return $this->db - ->hashtable(Project::TABLE) + ->hashtable(ProjectModel::TABLE) ->join(self::TABLE, 'project_id', 'id') - ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) - ->eq(GroupMember::TABLE.'.user_id', $user_id) - ->in(Project::TABLE.'.is_active', $status) - ->getAll(Project::TABLE.'.id', Project::TABLE.'.name'); + ->join(GroupMemberModel::TABLE, 'group_id', 'group_id', self::TABLE) + ->eq(GroupMemberModel::TABLE.'.user_id', $user_id) + ->in(ProjectModel::TABLE.'.is_active', $status) + ->getAll(ProjectModel::TABLE.'.id', ProjectModel::TABLE.'.name'); } /** @@ -49,8 +50,8 @@ class ProjectGroupRole extends Base public function getUserRole($project_id, $user_id) { $roles = $this->db->table(self::TABLE) - ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) - ->eq(GroupMember::TABLE.'.user_id', $user_id) + ->join(GroupMemberModel::TABLE, 'group_id', 'group_id', self::TABLE) + ->eq(GroupMemberModel::TABLE.'.user_id', $user_id) ->eq(self::TABLE.'.project_id', $project_id) ->findAllByColumn('role'); @@ -67,8 +68,8 @@ class ProjectGroupRole extends Base public function getGroups($project_id) { return $this->db->table(self::TABLE) - ->columns(Group::TABLE.'.id', Group::TABLE.'.name', self::TABLE.'.role') - ->join(Group::TABLE, 'id', 'group_id') + ->columns(GroupModel::TABLE.'.id', GroupModel::TABLE.'.name', self::TABLE.'.role') + ->join(GroupModel::TABLE, 'id', 'group_id') ->eq('project_id', $project_id) ->asc('name') ->findAll(); @@ -84,11 +85,11 @@ class ProjectGroupRole extends Base public function getUsers($project_id) { return $this->db->table(self::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role') - ->join(GroupMember::TABLE, 'group_id', 'group_id', self::TABLE) - ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) + ->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name', self::TABLE.'.role') + ->join(GroupMemberModel::TABLE, 'group_id', 'group_id', self::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id', GroupMemberModel::TABLE) ->eq(self::TABLE.'.project_id', $project_id) - ->asc(User::TABLE.'.username') + ->asc(UserModel::TABLE.'.username') ->findAll(); } @@ -101,14 +102,14 @@ class ProjectGroupRole extends Base */ public function getAssignableUsers($project_id) { - return $this->db->table(User::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') - ->join(GroupMember::TABLE, 'user_id', 'id', User::TABLE) - ->join(self::TABLE, 'group_id', 'group_id', GroupMember::TABLE) + return $this->db->table(UserModel::TABLE) + ->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name') + ->join(GroupMemberModel::TABLE, 'user_id', 'id', UserModel::TABLE) + ->join(self::TABLE, 'group_id', 'group_id', GroupMemberModel::TABLE) ->eq(self::TABLE.'.project_id', $project_id) - ->eq(User::TABLE.'.is_active', 1) + ->eq(UserModel::TABLE.'.is_active', 1) ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) - ->asc(User::TABLE.'.username') + ->asc(UserModel::TABLE.'.username') ->findAll(); } diff --git a/sources/app/Model/ProjectMetadata.php b/sources/app/Model/ProjectMetadata.php deleted file mode 100644 index 8549805..0000000 --- a/sources/app/Model/ProjectMetadata.php +++ /dev/null @@ -1,30 +0,0 @@ -getAll($src_project_id); + + if (! $this->save($dst_project_id, $metadata)) { + return false; + } + + return true; + } +} diff --git a/sources/app/Model/Project.php b/sources/app/Model/ProjectModel.php similarity index 88% rename from sources/app/Model/Project.php rename to sources/app/Model/ProjectModel.php index d2e5b7c..850531c 100644 --- a/sources/app/Model/Project.php +++ b/sources/app/Model/ProjectModel.php @@ -2,16 +2,17 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; /** * Project model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Project extends Base +class ProjectModel extends Base { /** * SQL table name for projects @@ -34,6 +35,20 @@ class Project extends Base */ const INACTIVE = 0; + /** + * Value for private project + * + * @var integer + */ + const TYPE_PRIVATE = 1; + + /** + * Value for team project + * + * @var integer + */ + const TYPE_TEAM = 0; + /** * Get a project by the id * @@ -56,9 +71,9 @@ class Project extends Base public function getByIdWithOwner($project_id) { return $this->db->table(self::TABLE) - ->columns(self::TABLE.'.*', User::TABLE.'.username AS owner_username', User::TABLE.'.name AS owner_name') + ->columns(self::TABLE.'.*', UserModel::TABLE.'.username AS owner_username', UserModel::TABLE.'.name AS owner_name') ->eq(self::TABLE.'.id', $project_id) - ->join(User::TABLE, 'id', 'owner_id') + ->join(UserModel::TABLE, 'id', 'owner_id') ->findOne(); } @@ -187,7 +202,7 @@ class Project extends Base * Get all projects with all its data for a given status * * @access public - * @param integer $status Proejct status: self::ACTIVE or self:INACTIVE + * @param integer $status Project status: self::ACTIVE or self:INACTIVE * @return array */ public function getAllByStatus($status) @@ -241,8 +256,8 @@ class Project extends Base { $stats = array(); $stats['nb_active_tasks'] = 0; - $columns = $this->column->getAll($project_id); - $column_stats = $this->board->getColumnStats($project_id); + $columns = $this->columnModel->getAll($project_id); + $column_stats = $this->boardModel->getColumnStats($project_id); foreach ($columns as &$column) { $column['nb_active_tasks'] = isset($column_stats[$column['id']]) ? $column_stats[$column['id']] : 0; @@ -250,7 +265,7 @@ class Project extends Base } $stats['columns'] = $columns; - $stats['nb_tasks'] = $this->taskFinder->countByProjectId($project_id); + $stats['nb_tasks'] = $this->taskFinderModel->countByProjectId($project_id); $stats['nb_inactive_tasks'] = $stats['nb_tasks'] - $stats['nb_active_tasks']; return $stats; @@ -265,11 +280,13 @@ class Project extends Base */ public function getColumnStats(array &$project) { - $project['columns'] = $this->column->getAll($project['id']); - $stats = $this->board->getColumnStats($project['id']); + $project['columns'] = $this->columnModel->getAll($project['id']); + $project['nb_active_tasks'] = 0; + $stats = $this->boardModel->getColumnStats($project['id']); foreach ($project['columns'] as &$column) { $column['nb_tasks'] = isset($stats[$column['id']]) ? $stats[$column['id']] : 0; + $project['nb_active_tasks'] += $column['nb_tasks']; } return $project; @@ -301,13 +318,13 @@ class Project extends Base public function getQueryColumnStats(array $project_ids) { if (empty($project_ids)) { - return $this->db->table(Project::TABLE)->limit(0); + return $this->db->table(ProjectModel::TABLE)->limit(0); } return $this->db - ->table(Project::TABLE) - ->columns(self::TABLE.'.*', User::TABLE.'.username AS owner_username', User::TABLE.'.name AS owner_name') - ->join(User::TABLE, 'id', 'owner_id') + ->table(ProjectModel::TABLE) + ->columns(self::TABLE.'.*', UserModel::TABLE.'.username AS owner_username', UserModel::TABLE.'.name AS owner_name') + ->join(UserModel::TABLE, 'id', 'owner_id') ->in(self::TABLE.'.id', $project_ids) ->callback(array($this, 'applyColumnStats')); } @@ -343,16 +360,16 @@ class Project extends Base $project_id = $this->db->getLastId(); - if (! $this->board->create($project_id, $this->board->getUserColumns())) { + if (! $this->boardModel->create($project_id, $this->boardModel->getUserColumns())) { $this->db->cancelTransaction(); return false; } if ($add_user && $user_id) { - $this->projectUserRole->addUser($project_id, $user_id, Role::PROJECT_MANAGER); + $this->projectUserRoleModel->addUser($project_id, $user_id, Role::PROJECT_MANAGER); } - $this->category->createDefaultCategories($project_id); + $this->categoryModel->createDefaultCategories($project_id); $this->db->closeTransaction(); diff --git a/sources/app/Model/ProjectNotification.php b/sources/app/Model/ProjectNotificationModel.php similarity index 68% rename from sources/app/Model/ProjectNotification.php rename to sources/app/Model/ProjectNotificationModel.php index a355902..aeeee4c 100644 --- a/sources/app/Model/ProjectNotification.php +++ b/sources/app/Model/ProjectNotificationModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Project Notification * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class ProjectNotification extends Base +class ProjectNotificationModel extends Base { /** * Send notifications @@ -20,15 +22,15 @@ class ProjectNotification extends Base */ public function sendNotifications($project_id, $event_name, array $event_data) { - $project = $this->project->getById($project_id); + $project = $this->projectModel->getById($project_id); $types = array_merge( - $this->projectNotificationType->getHiddenTypes(), - $this->projectNotificationType->getSelectedTypes($project_id) + $this->projectNotificationTypeModel->getHiddenTypes(), + $this->projectNotificationTypeModel->getSelectedTypes($project_id) ); foreach ($types as $type) { - $this->projectNotificationType->getType($type)->notifyProject($project, $event_name, $event_data); + $this->projectNotificationTypeModel->getType($type)->notifyProject($project, $event_name, $event_data); } } @@ -44,7 +46,7 @@ class ProjectNotification extends Base $this->db->startTransaction(); $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']); - $this->projectNotificationType->saveSelectedTypes($project_id, $types); + $this->projectNotificationTypeModel->saveSelectedTypes($project_id, $types); $this->db->closeTransaction(); } @@ -59,7 +61,7 @@ class ProjectNotification extends Base public function readSettings($project_id) { return array( - 'notification_types' => $this->projectNotificationType->getSelectedTypes($project_id), + 'notification_types' => $this->projectNotificationTypeModel->getSelectedTypes($project_id), ); } } diff --git a/sources/app/Model/ProjectNotificationType.php b/sources/app/Model/ProjectNotificationTypeModel.php similarity index 93% rename from sources/app/Model/ProjectNotificationType.php rename to sources/app/Model/ProjectNotificationTypeModel.php index a471959..aeec77f 100644 --- a/sources/app/Model/ProjectNotificationType.php +++ b/sources/app/Model/ProjectNotificationTypeModel.php @@ -5,10 +5,10 @@ namespace Kanboard\Model; /** * Project Notification Type * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class ProjectNotificationType extends NotificationType +class ProjectNotificationTypeModel extends NotificationTypeModel { /** * SQL table name diff --git a/sources/app/Model/ProjectPermission.php b/sources/app/Model/ProjectPermissionModel.php similarity index 56% rename from sources/app/Model/ProjectPermission.php rename to sources/app/Model/ProjectPermissionModel.php index db1573a..a7c1857 100644 --- a/sources/app/Model/ProjectPermission.php +++ b/sources/app/Model/ProjectPermissionModel.php @@ -2,15 +2,20 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Core\Security\Role; +use Kanboard\Filter\ProjectGroupRoleProjectFilter; +use Kanboard\Filter\ProjectGroupRoleUsernameFilter; +use Kanboard\Filter\ProjectUserRoleProjectFilter; +use Kanboard\Filter\ProjectUserRoleUsernameFilter; /** * Project Permission * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class ProjectPermission extends Base +class ProjectPermissionModel extends Base { /** * Get query for project users overview @@ -28,18 +33,18 @@ class ProjectPermission extends Base return $this ->db - ->table(ProjectUserRole::TABLE) - ->join(User::TABLE, 'id', 'user_id') - ->join(Project::TABLE, 'id', 'project_id') - ->eq(ProjectUserRole::TABLE.'.role', $role) - ->eq(Project::TABLE.'.is_private', 0) - ->in(Project::TABLE.'.id', $project_ids) + ->table(ProjectUserRoleModel::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id') + ->join(ProjectModel::TABLE, 'id', 'project_id') + ->eq(ProjectUserRoleModel::TABLE.'.role', $role) + ->eq(ProjectModel::TABLE.'.is_private', 0) + ->in(ProjectModel::TABLE.'.id', $project_ids) ->columns( - User::TABLE.'.id', - User::TABLE.'.username', - User::TABLE.'.name', - Project::TABLE.'.name AS project_name', - Project::TABLE.'.id' + UserModel::TABLE.'.id', + UserModel::TABLE.'.username', + UserModel::TABLE.'.name', + ProjectModel::TABLE.'.name AS project_name', + ProjectModel::TABLE.'.id' ); } @@ -53,8 +58,18 @@ class ProjectPermission extends Base */ public function findUsernames($project_id, $input) { - $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); - $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username'); + $userMembers = $this->projectUserRoleQuery + ->withFilter(new ProjectUserRoleProjectFilter($project_id)) + ->withFilter(new ProjectUserRoleUsernameFilter($input)) + ->getQuery() + ->findAllByColumn('username'); + + $groupMembers = $this->projectGroupRoleQuery + ->withFilter(new ProjectGroupRoleProjectFilter($project_id)) + ->withFilter(new ProjectGroupRoleUsernameFilter($input)) + ->getQuery() + ->findAllByColumn('username'); + $members = array_unique(array_merge($userMembers, $groupMembers)); sort($members); @@ -72,7 +87,7 @@ class ProjectPermission extends Base public function isEverybodyAllowed($project_id) { return $this->db - ->table(Project::TABLE) + ->table(ProjectModel::TABLE) ->eq('id', $project_id) ->eq('is_everybody_allowed', 1) ->exists(); @@ -92,7 +107,7 @@ class ProjectPermission extends Base } return in_array( - $this->projectUserRole->getUserRole($project_id, $user_id), + $this->projectUserRoleModel->getUserRole($project_id, $user_id), array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER, Role::PROJECT_VIEWER) ); } @@ -107,8 +122,8 @@ class ProjectPermission extends Base */ public function isAssignable($project_id, $user_id) { - return $this->user->isActive($user_id) && - in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); + return $this->userModel->isActive($user_id) && + in_array($this->projectUserRoleModel->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER)); } /** @@ -121,7 +136,7 @@ class ProjectPermission extends Base */ public function isMember($project_id, $user_id) { - return in_array($this->projectUserRole->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER, Role::PROJECT_VIEWER)); + return in_array($this->projectUserRoleModel->getUserRole($project_id, $user_id), array(Role::PROJECT_MEMBER, Role::PROJECT_MANAGER, Role::PROJECT_VIEWER)); } /** @@ -133,7 +148,7 @@ class ProjectPermission extends Base */ public function getActiveProjectIds($user_id) { - return array_keys($this->projectUserRole->getActiveProjectsByUser($user_id)); + return array_keys($this->projectUserRoleModel->getActiveProjectsByUser($user_id)); } /** @@ -145,7 +160,7 @@ class ProjectPermission extends Base */ public function duplicate($project_src_id, $project_dst_id) { - return $this->projectUserRole->duplicate($project_src_id, $project_dst_id) && - $this->projectGroupRole->duplicate($project_src_id, $project_dst_id); + return $this->projectUserRoleModel->duplicate($project_src_id, $project_dst_id) && + $this->projectGroupRoleModel->duplicate($project_src_id, $project_dst_id); } } diff --git a/sources/app/Model/ProjectTaskDuplicationModel.php b/sources/app/Model/ProjectTaskDuplicationModel.php new file mode 100644 index 0000000..5d2e132 --- /dev/null +++ b/sources/app/Model/ProjectTaskDuplicationModel.php @@ -0,0 +1,35 @@ +taskFinderModel->getAllIds($src_project_id, array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED)); + + foreach ($task_ids as $task_id) { + if (! $this->taskProjectDuplicationModel->duplicateToProject($task_id, $dst_project_id)) { + return false; + } + } + + return true; + } +} diff --git a/sources/app/Model/ProjectTaskPriorityModel.php b/sources/app/Model/ProjectTaskPriorityModel.php new file mode 100644 index 0000000..c1a0257 --- /dev/null +++ b/sources/app/Model/ProjectTaskPriorityModel.php @@ -0,0 +1,74 @@ +db + ->table(ProjectModel::TABLE) + ->columns('priority_default', 'priority_start', 'priority_end') + ->eq('id', $project_id) + ->findOne(); + } + + /** + * Get default task priority + * + * @access public + * @param int $project_id + * @return int + */ + public function getDefaultPriority($project_id) + { + return $this->db->table(ProjectModel::TABLE)->eq('id', $project_id)->findOneColumn('priority_default') ?: 0; + } + + /** + * Get priority for a destination project + * + * @access public + * @param integer $dst_project_id + * @param integer $priority + * @return integer + */ + public function getPriorityForProject($dst_project_id, $priority) + { + $settings = $this->getPrioritySettings($dst_project_id); + + if ($priority >= $settings['priority_start'] && $priority <= $settings['priority_end']) { + return $priority; + } + + return $settings['priority_default']; + } +} diff --git a/sources/app/Model/ProjectUserRoleFilter.php b/sources/app/Model/ProjectUserRoleFilter.php deleted file mode 100644 index 6440364..0000000 --- a/sources/app/Model/ProjectUserRoleFilter.php +++ /dev/null @@ -1,88 +0,0 @@ -query = $this->db->table(ProjectUserRole::TABLE); - return $this; - } - - /** - * Get all results of the filter - * - * @access public - * @param string $column - * @return array - */ - public function findAll($column = '') - { - if ($column !== '') { - return $this->query->asc($column)->findAllByColumn($column); - } - - return $this->query->findAll(); - } - - /** - * Get the PicoDb query - * - * @access public - * @return \PicoDb\Table - */ - public function getQuery() - { - return $this->query; - } - - /** - * Filter by project id - * - * @access public - * @param integer $project_id - * @return ProjectUserRoleFilter - */ - public function filterByProjectId($project_id) - { - $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id); - return $this; - } - - /** - * Filter by username - * - * @access public - * @param string $input - * @return ProjectUserRoleFilter - */ - public function startWithUsername($input) - { - $this->query - ->join(User::TABLE, 'id', 'user_id') - ->ilike(User::TABLE.'.username', $input.'%'); - - return $this; - } -} diff --git a/sources/app/Model/ProjectUserRole.php b/sources/app/Model/ProjectUserRoleModel.php similarity index 74% rename from sources/app/Model/ProjectUserRole.php rename to sources/app/Model/ProjectUserRoleModel.php index 56da679..a0df0cf 100644 --- a/sources/app/Model/ProjectUserRole.php +++ b/sources/app/Model/ProjectUserRoleModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Core\Security\Role; /** * Project User Role * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class ProjectUserRole extends Base +class ProjectUserRoleModel extends Base { /** * SQL table name @@ -28,7 +29,7 @@ class ProjectUserRole extends Base */ public function getActiveProjectsByUser($user_id) { - return $this->getProjectsByUser($user_id, array(Project::ACTIVE)); + return $this->getProjectsByUser($user_id, array(ProjectModel::ACTIVE)); } /** @@ -39,19 +40,19 @@ class ProjectUserRole extends Base * @param array $status * @return array */ - public function getProjectsByUser($user_id, $status = array(Project::ACTIVE, Project::INACTIVE)) + public function getProjectsByUser($user_id, $status = array(ProjectModel::ACTIVE, ProjectModel::INACTIVE)) { $userProjects = $this->db - ->hashtable(Project::TABLE) + ->hashtable(ProjectModel::TABLE) ->beginOr() ->eq(self::TABLE.'.user_id', $user_id) - ->eq(Project::TABLE.'.is_everybody_allowed', 1) + ->eq(ProjectModel::TABLE.'.is_everybody_allowed', 1) ->closeOr() - ->in(Project::TABLE.'.is_active', $status) + ->in(ProjectModel::TABLE.'.is_active', $status) ->join(self::TABLE, 'project_id', 'id') - ->getAll(Project::TABLE.'.id', Project::TABLE.'.name'); + ->getAll(ProjectModel::TABLE.'.id', ProjectModel::TABLE.'.name'); - $groupProjects = $this->projectGroupRole->getProjectsByUser($user_id, $status); + $groupProjects = $this->projectGroupRoleModel->getProjectsByUser($user_id, $status); $projects = $userProjects + $groupProjects; asort($projects); @@ -69,14 +70,19 @@ class ProjectUserRole extends Base */ public function getUserRole($project_id, $user_id) { - if ($this->projectPermission->isEverybodyAllowed($project_id)) { - return Role::PROJECT_MEMBER; + $projectInfo = $this->db->table(ProjectModel::TABLE) + ->eq('id', $project_id) + ->columns('owner_id', 'is_everybody_allowed') + ->findOne(); + + if ($projectInfo['is_everybody_allowed'] == 1) { + return $projectInfo['owner_id'] == $user_id ? Role::PROJECT_MANAGER : Role::PROJECT_MEMBER; } $role = $this->db->table(self::TABLE)->eq('user_id', $user_id)->eq('project_id', $project_id)->findOneColumn('role'); if (empty($role)) { - $role = $this->projectGroupRole->getUserRole($project_id, $user_id); + $role = $this->projectGroupRoleModel->getUserRole($project_id, $user_id); } return $role; @@ -92,11 +98,11 @@ class ProjectUserRole extends Base public function getUsers($project_id) { return $this->db->table(self::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', self::TABLE.'.role') - ->join(User::TABLE, 'id', 'user_id') + ->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name', self::TABLE.'.role') + ->join(UserModel::TABLE, 'id', 'user_id') ->eq('project_id', $project_id) - ->asc(User::TABLE.'.username') - ->asc(User::TABLE.'.name') + ->asc(UserModel::TABLE.'.username') + ->asc(UserModel::TABLE.'.name') ->findAll(); } @@ -110,10 +116,10 @@ class ProjectUserRole extends Base public function getAllUsers($project_id) { $userMembers = $this->getUsers($project_id); - $groupMembers = $this->projectGroupRole->getUsers($project_id); + $groupMembers = $this->projectGroupRoleModel->getUsers($project_id); $members = array_merge($userMembers, $groupMembers); - return $this->user->prepareList($members); + return $this->userModel->prepareList($members); } /** @@ -128,7 +134,7 @@ class ProjectUserRole extends Base $users = array(); $userMembers = $this->getUsers($project_id); - $groupMembers = $this->projectGroupRole->getUsers($project_id); + $groupMembers = $this->projectGroupRoleModel->getUsers($project_id); $members = array_merge($userMembers, $groupMembers); foreach ($members as $user) { @@ -151,22 +157,22 @@ class ProjectUserRole extends Base */ public function getAssignableUsers($project_id) { - if ($this->projectPermission->isEverybodyAllowed($project_id)) { - return $this->user->getActiveUsersList(); + if ($this->projectPermissionModel->isEverybodyAllowed($project_id)) { + return $this->userModel->getActiveUsersList(); } $userMembers = $this->db->table(self::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') - ->join(User::TABLE, 'id', 'user_id') - ->eq(User::TABLE.'.is_active', 1) + ->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name') + ->join(UserModel::TABLE, 'id', 'user_id') + ->eq(UserModel::TABLE.'.is_active', 1) ->eq(self::TABLE.'.project_id', $project_id) ->in(self::TABLE.'.role', array(Role::PROJECT_MANAGER, Role::PROJECT_MEMBER)) ->findAll(); - $groupMembers = $this->projectGroupRole->getAssignableUsers($project_id); + $groupMembers = $this->projectGroupRoleModel->getAssignableUsers($project_id); $members = array_merge($userMembers, $groupMembers); - return $this->user->prepareList($members); + return $this->userModel->prepareList($members); } /** @@ -192,7 +198,7 @@ class ProjectUserRole extends Base } if ($everybody) { - $users = array(User::EVERYBODY_ID => t('Everybody')) + $users; + $users = array(UserModel::EVERYBODY_ID => t('Everybody')) + $users; } return $users; @@ -251,8 +257,8 @@ class ProjectUserRole extends Base /** * Copy user access from a project to another one * - * @param integer $project_src_id Project Template - * @return integer $project_dst_id Project that receives the copy + * @param integer $project_src_id + * @param integer $project_dst_id * @return boolean */ public function duplicate($project_src_id, $project_dst_id) diff --git a/sources/app/Model/RememberMeSession.php b/sources/app/Model/RememberMeSessionModel.php similarity index 97% rename from sources/app/Model/RememberMeSession.php rename to sources/app/Model/RememberMeSessionModel.php index 8989a6d..f6c8d64 100644 --- a/sources/app/Model/RememberMeSession.php +++ b/sources/app/Model/RememberMeSessionModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Core\Security\Token; /** * Remember Me Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class RememberMeSession extends Base +class RememberMeSessionModel extends Base { /** * SQL table name diff --git a/sources/app/Model/Setting.php b/sources/app/Model/SettingModel.php similarity index 95% rename from sources/app/Model/Setting.php rename to sources/app/Model/SettingModel.php index f98d7ce..5b2ee54 100644 --- a/sources/app/Model/Setting.php +++ b/sources/app/Model/SettingModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Application Settings * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -abstract class Setting extends Base +abstract class SettingModel extends Base { /** * SQL table name @@ -22,6 +24,7 @@ abstract class Setting extends Base * * @abstract * @access public + * @param array $values * @return array */ abstract public function prepare(array $values); diff --git a/sources/app/Model/Subtask.php b/sources/app/Model/SubtaskModel.php similarity index 78% rename from sources/app/Model/Subtask.php rename to sources/app/Model/SubtaskModel.php index 3f5cfe8..a97bddb 100644 --- a/sources/app/Model/Subtask.php +++ b/sources/app/Model/SubtaskModel.php @@ -3,15 +3,16 @@ namespace Kanboard\Model; use PicoDb\Database; +use Kanboard\Core\Base; use Kanboard\Event\SubtaskEvent; /** * Subtask Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Subtask extends Base +class SubtaskModel extends Base { /** * SQL table name @@ -50,6 +51,22 @@ class Subtask extends Base const EVENT_CREATE = 'subtask.create'; const EVENT_DELETE = 'subtask.delete'; + /** + * Get projectId from subtaskId + * + * @access public + * @param integer $subtask_id + * @return integer + */ + public function getProjectId($subtask_id) + { + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $subtask_id) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; + } + /** * Get available status * @@ -95,20 +112,20 @@ class Subtask extends Base */ public function getUserQuery($user_id, array $status) { - return $this->db->table(Subtask::TABLE) + return $this->db->table(SubtaskModel::TABLE) ->columns( - Subtask::TABLE.'.*', - Task::TABLE.'.project_id', - Task::TABLE.'.color_id', - Task::TABLE.'.title AS task_name', - Project::TABLE.'.name AS project_name' + SubtaskModel::TABLE.'.*', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.color_id', + TaskModel::TABLE.'.title AS task_name', + ProjectModel::TABLE.'.name AS project_name' ) - ->subquery($this->subtaskTimeTracking->getTimerQuery($user_id), 'timer_start_date') + ->subquery($this->subtaskTimeTrackingModel->getTimerQuery($user_id), 'timer_start_date') ->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) + ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE) + ->in(SubtaskModel::TABLE.'.status', $status) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE) ->callback(array($this, 'addStatusName')); } @@ -126,11 +143,11 @@ class Subtask extends Base ->eq('task_id', $task_id) ->columns( self::TABLE.'.*', - User::TABLE.'.username', - User::TABLE.'.name' + UserModel::TABLE.'.username', + UserModel::TABLE.'.name' ) - ->subquery($this->subtaskTimeTracking->getTimerQuery($this->userSession->getId()), 'timer_start_date') - ->join(User::TABLE, 'id', 'user_id') + ->subquery($this->subtaskTimeTrackingModel->getTimerQuery($this->userSession->getId()), 'timer_start_date') + ->join(UserModel::TABLE, 'id', 'user_id') ->asc(self::TABLE.'.position') ->callback(array($this, 'addStatusName')) ->findAll(); @@ -150,9 +167,9 @@ class Subtask extends Base return $this->db ->table(self::TABLE) ->eq(self::TABLE.'.id', $subtask_id) - ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') - ->subquery($this->subtaskTimeTracking->getTimerQuery($this->userSession->getId()), 'timer_start_date') - ->join(User::TABLE, 'id', 'user_id') + ->columns(self::TABLE.'.*', UserModel::TABLE.'.username', UserModel::TABLE.'.name') + ->subquery($this->subtaskTimeTrackingModel->getTimerQuery($this->userSession->getId()), 'timer_start_date') + ->join(UserModel::TABLE, 'id', 'user_id') ->callback(array($this, 'addStatusName')) ->findOne(); } @@ -215,9 +232,9 @@ class Subtask extends Base public function create(array $values) { $this->prepareCreation($values); - $subtask_id = $this->persist(self::TABLE, $values); + $subtask_id = $this->db->table(self::TABLE)->persist($values); - if ($subtask_id) { + if ($subtask_id !== false) { $this->container['dispatcher']->dispatch( self::EVENT_CREATE, new SubtaskEvent(array('id' => $subtask_id) + $values) @@ -344,7 +361,7 @@ class Subtask extends Base */ public function hasSubtaskInProgress($user_id) { - return $this->config->get('subtask_restriction') == 1 && + return $this->configModel->get('subtask_restriction') == 1 && $this->db->table(self::TABLE) ->eq('status', self::STATUS_INPROGRESS) ->eq('user_id', $user_id) @@ -382,7 +399,7 @@ class Subtask extends Base { return $this->db->transaction(function (Database $db) use ($src_task_id, $dst_task_id) { - $subtasks = $db->table(Subtask::TABLE) + $subtasks = $db->table(SubtaskModel::TABLE) ->columns('title', 'time_estimated', 'position') ->eq('task_id', $src_task_id) ->asc('position') @@ -391,10 +408,37 @@ class Subtask extends Base foreach ($subtasks as &$subtask) { $subtask['task_id'] = $dst_task_id; - if (! $db->table(Subtask::TABLE)->save($subtask)) { + if (! $db->table(SubtaskModel::TABLE)->save($subtask)) { return false; } } }); } + + /** + * Convert a subtask to a task + * + * @access public + * @param integer $project_id + * @param integer $subtask_id + * @return integer + */ + public function convertToTask($project_id, $subtask_id) + { + $subtask = $this->getById($subtask_id); + + $task_id = $this->taskCreationModel->create(array( + 'project_id' => $project_id, + 'title' => $subtask['title'], + 'time_estimated' => $subtask['time_estimated'], + 'time_spent' => $subtask['time_spent'], + 'owner_id' => $subtask['user_id'], + )); + + if ($task_id !== false) { + $this->remove($subtask_id); + } + + return $task_id; + } } diff --git a/sources/app/Model/SubtaskTimeTracking.php b/sources/app/Model/SubtaskTimeTrackingModel.php similarity index 60% rename from sources/app/Model/SubtaskTimeTracking.php rename to sources/app/Model/SubtaskTimeTrackingModel.php index b766b54..062e594 100644 --- a/sources/app/Model/SubtaskTimeTracking.php +++ b/sources/app/Model/SubtaskTimeTrackingModel.php @@ -3,14 +3,15 @@ namespace Kanboard\Model; use DateTime; +use Kanboard\Core\Base; /** - * Subtask timesheet + * Subtask time tracking * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class SubtaskTimeTracking extends Base +class SubtaskTimeTrackingModel extends Base { /** * SQL table name @@ -36,7 +37,7 @@ class SubtaskTimeTracking extends Base $user_id, $this->db->escapeIdentifier('end'), $this->db->escapeIdentifier('subtask_id'), - Subtask::TABLE.'.id' + SubtaskModel::TABLE.'.id' ); } @@ -57,14 +58,14 @@ class SubtaskTimeTracking extends Base 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' + SubtaskModel::TABLE.'.task_id', + SubtaskModel::TABLE.'.title AS subtask_title', + TaskModel::TABLE.'.title AS task_title', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.color_id' ) - ->join(Subtask::TABLE, 'id', 'subtask_id') - ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE) + ->join(SubtaskModel::TABLE, 'id', 'subtask_id') + ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) ->eq(self::TABLE.'.user_id', $user_id); } @@ -86,16 +87,16 @@ class SubtaskTimeTracking extends Base 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' + SubtaskModel::TABLE.'.task_id', + SubtaskModel::TABLE.'.title AS subtask_title', + TaskModel::TABLE.'.project_id', + UserModel::TABLE.'.username', + UserModel::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); + ->join(SubtaskModel::TABLE, 'id', 'subtask_id') + ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id', self::TABLE) + ->eq(TaskModel::TABLE.'.id', $task_id); } /** @@ -116,17 +117,17 @@ class SubtaskTimeTracking extends Base 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' + SubtaskModel::TABLE.'.task_id', + SubtaskModel::TABLE.'.title AS subtask_title', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.color_id', + UserModel::TABLE.'.username', + UserModel::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) + ->join(SubtaskModel::TABLE, 'id', 'subtask_id') + ->join(TaskModel::TABLE, 'id', 'task_id', SubtaskModel::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id', self::TABLE) + ->eq(TaskModel::TABLE.'.project_id', $project_id) ->asc(self::TABLE.'.id'); } @@ -145,94 +146,6 @@ class SubtaskTimeTracking extends Base ->findAll(); } - /** - * Get user calendar events - * - * @access public - * @param integer $user_id - * @param string $start ISO-8601 format - * @param string $end - * @return array - */ - public function getUserCalendarEvents($user_id, $start, $end) - { - $hook = 'model:subtask-time-tracking:calendar:events'; - $events = $this->getUserQuery($user_id) - ->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'start', - 'end' - )) - ->findAll(); - - if ($this->hook->exists($hook)) { - $events = $this->hook->first($hook, array( - 'user_id' => $user_id, - 'events' => $events, - 'start' => $start, - 'end' => $end, - )); - } - - return $this->toCalendarEvents($events); - } - - /** - * Get project calendar events - * - * @access public - * @param integer $project_id - * @param integer $start - * @param integer $end - * @return array - */ - public function getProjectCalendarEvents($project_id, $start, $end) - { - $result = $this - ->getProjectQuery($project_id) - ->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'start', - 'end' - )) - ->findAll(); - - return $this->toCalendarEvents($result); - } - - /** - * Convert a record set to calendar events - * - * @access private - * @param array $rows - * @return array - */ - private function toCalendarEvents(array $rows) - { - $events = array(); - - foreach ($rows as $row) { - $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : ''; - - $events[] = array( - 'id' => $row['id'], - 'subtask_id' => $row['subtask_id'], - 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user, - 'start' => date('Y-m-d\TH:i:s', $row['start']), - 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()), - 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']), - 'borderColor' => $this->color->getBorderColor($row['color_id']), - 'textColor' => 'black', - 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])), - 'editable' => false, - ); - } - - return $events; - } - /** * Return true if a timer is started for this use and subtask * @@ -337,10 +250,10 @@ class SubtaskTimeTracking extends Base */ public function updateSubtaskTimeSpent($subtask_id, $time_spent) { - $subtask = $this->subtask->getById($subtask_id); + $subtask = $this->subtaskModel->getById($subtask_id); // Fire the event subtask.update - return $this->subtask->update(array( + return $this->subtaskModel->update(array( 'id' => $subtask['id'], 'time_spent' => $subtask['time_spent'] + $time_spent, 'task_id' => $subtask['task_id'], @@ -359,7 +272,7 @@ class SubtaskTimeTracking extends Base $values = $this->calculateSubtaskTime($task_id); return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->eq('id', $task_id) ->update($values); } @@ -374,7 +287,7 @@ class SubtaskTimeTracking extends Base public function calculateSubtaskTime($task_id) { return $this->db - ->table(Subtask::TABLE) + ->table(SubtaskModel::TABLE) ->eq('task_id', $task_id) ->columns( 'SUM(time_spent) AS time_spent', diff --git a/sources/app/Model/Swimlane.php b/sources/app/Model/SwimlaneModel.php similarity index 90% rename from sources/app/Model/Swimlane.php rename to sources/app/Model/SwimlaneModel.php index 721f20d..f20bfa2 100644 --- a/sources/app/Model/Swimlane.php +++ b/sources/app/Model/SwimlaneModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Swimlanes * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Swimlane extends Base +class SwimlaneModel extends Base { /** * SQL table name @@ -87,6 +89,24 @@ class Swimlane extends Base ->findOne(); } + /** + * Get first active swimlane for a project + * + * @access public + * @param integer $project_id + * @return array|null + */ + public function getFirstActiveSwimlane($project_id) + { + $swimlanes = $this->getSwimlanes($project_id); + + if (empty($swimlanes)) { + return null; + } + + return $swimlanes[0]; + } + /** * Get default swimlane properties * @@ -97,7 +117,7 @@ class Swimlane extends Base public function getDefault($project_id) { $result = $this->db - ->table(Project::TABLE) + ->table(ProjectModel::TABLE) ->eq('id', $project_id) ->columns('id', 'default_swimlane', 'show_default_swimlane') ->findOne(); @@ -166,18 +186,18 @@ class Swimlane extends Base ->orderBy('position', 'asc') ->findAll(); - $default_swimlane = $this->db - ->table(Project::TABLE) + $defaultSwimlane = $this->db + ->table(ProjectModel::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); + if ($defaultSwimlane) { + if ($defaultSwimlane === 'Default swimlane') { + $defaultSwimlane = t($defaultSwimlane); } - array_unshift($swimlanes, array('id' => 0, 'name' => $default_swimlane)); + array_unshift($swimlanes, array('id' => 0, 'name' => $defaultSwimlane)); } return $swimlanes; @@ -195,7 +215,7 @@ class Swimlane extends Base 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'); + $default = $this->db->table(ProjectModel::TABLE)->eq('id', $project_id)->eq('show_default_swimlane', 1)->findOneColumn('default_swimlane'); if ($prepend) { $swimlanes[-1] = t('All swimlanes'); @@ -222,11 +242,12 @@ class Swimlane extends Base */ public function create($values) { - if (! $this->project->exists($values['project_id'])) { + if (! $this->projectModel->exists($values['project_id'])) { return 0; } + $values['position'] = $this->getLastPosition($values['project_id']); - return $this->persist(self::TABLE, $values); + return $this->db->table(self::TABLE)->persist($values); } /** @@ -254,7 +275,7 @@ class Swimlane extends Base public function updateDefault(array $values) { return $this->db - ->table(Project::TABLE) + ->table(ProjectModel::TABLE) ->eq('id', $values['id']) ->update(array( 'default_swimlane' => $values['default_swimlane'], @@ -272,7 +293,7 @@ class Swimlane extends Base public function enableDefault($project_id) { return $this->db - ->table(Project::TABLE) + ->table(ProjectModel::TABLE) ->eq('id', $project_id) ->update(array( 'show_default_swimlane' => 1, @@ -289,7 +310,7 @@ class Swimlane extends Base public function disableDefault($project_id) { return $this->db - ->table(Project::TABLE) + ->table(ProjectModel::TABLE) ->eq('id', $project_id) ->update(array( 'show_default_swimlane' => 0, @@ -370,7 +391,7 @@ class Swimlane extends Base $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)); + $this->db->table(TaskModel::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(); diff --git a/sources/app/Model/TagDuplicationModel.php b/sources/app/Model/TagDuplicationModel.php new file mode 100644 index 0000000..fb0d817 --- /dev/null +++ b/sources/app/Model/TagDuplicationModel.php @@ -0,0 +1,87 @@ +tagModel->getAllByProject($src_project_id); + $results = array(); + + foreach ($tags as $tag) { + $results[] = $this->tagModel->create($dst_project_id, $tag['name']); + } + + return ! in_array(false, $results, true); + } + + /** + * Link tags to the new tasks + * + * @access public + * @param integer $src_task_id + * @param integer $dst_task_id + * @param integer $dst_project_id + */ + public function duplicateTaskTagsToAnotherProject($src_task_id, $dst_task_id, $dst_project_id) + { + $tags = $this->taskTagModel->getTagsByTask($src_task_id); + + foreach ($tags as $tag) { + $tag_id = $this->tagModel->getIdByName($dst_project_id, $tag['name']); + + if ($tag_id) { + $this->taskTagModel->associateTag($dst_task_id, $tag_id); + } + } + } + + /** + * Duplicate tags to the new task + * + * @access public + * @param integer $src_task_id + * @param integer $dst_task_id + */ + public function duplicateTaskTags($src_task_id, $dst_task_id) + { + $tags = $this->taskTagModel->getTagsByTask($src_task_id); + + foreach ($tags as $tag) { + $this->taskTagModel->associateTag($dst_task_id, $tag['id']); + } + } + + /** + * Remove tags that are not available in destination project + * + * @access public + * @param integer $task_id + * @param integer $dst_project_id + */ + public function syncTaskTagsToAnotherProject($task_id, $dst_project_id) + { + $tag_ids = $this->taskTagModel->getTagIdsByTaskNotAvailableInProject($task_id, $dst_project_id); + + foreach ($tag_ids as $tag_id) { + $this->taskTagModel->dissociateTag($task_id, $tag_id); + } + } +} diff --git a/sources/app/Model/TagModel.php b/sources/app/Model/TagModel.php new file mode 100644 index 0000000..e85c5a8 --- /dev/null +++ b/sources/app/Model/TagModel.php @@ -0,0 +1,180 @@ +db->table(self::TABLE)->asc('name')->findAll(); + } + + /** + * Get all tags by project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAllByProject($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('name')->findAll(); + } + + /** + * Get assignable tags for a project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAssignableList($project_id) + { + return $this->db->hashtable(self::TABLE) + ->beginOr() + ->eq('project_id', $project_id) + ->eq('project_id', 0) + ->closeOr() + ->asc('name') + ->getAll('id', 'name'); + } + + /** + * Get one tag + * + * @access public + * @param integer $tag_id + * @return array|null + */ + public function getById($tag_id) + { + return $this->db->table(self::TABLE)->eq('id', $tag_id)->findOne(); + } + + /** + * Get tag id from tag name + * + * @access public + * @param int $project_id + * @param string $tag + * @return integer + */ + public function getIdByName($project_id, $tag) + { + return $this->db + ->table(self::TABLE) + ->beginOr() + ->eq('project_id', 0) + ->eq('project_id', $project_id) + ->closeOr() + ->ilike('name', $tag) + ->asc('project_id') + ->findOneColumn('id'); + } + + /** + * Return true if the tag exists + * + * @access public + * @param integer $project_id + * @param string $tag + * @param integer $tag_id + * @return boolean + */ + public function exists($project_id, $tag, $tag_id = 0) + { + return $this->db + ->table(self::TABLE) + ->neq('id', $tag_id) + ->beginOr() + ->eq('project_id', 0) + ->eq('project_id', $project_id) + ->closeOr() + ->ilike('name', $tag) + ->asc('project_id') + ->exists(); + } + + /** + * Return tag id and create a new tag if necessary + * + * @access public + * @param int $project_id + * @param string $tag + * @return bool|int + */ + public function findOrCreateTag($project_id, $tag) + { + $tag_id = $this->getIdByName($project_id, $tag); + + if (empty($tag_id)) { + $tag_id = $this->create($project_id, $tag); + } + + return $tag_id; + } + + /** + * Add a new tag + * + * @access public + * @param int $project_id + * @param string $tag + * @return bool|int + */ + public function create($project_id, $tag) + { + return $this->db->table(self::TABLE)->persist(array( + 'project_id' => $project_id, + 'name' => $tag, + )); + } + + /** + * Update a tag + * + * @access public + * @param integer $tag_id + * @param string $tag + * @return bool + */ + public function update($tag_id, $tag) + { + return $this->db->table(self::TABLE)->eq('id', $tag_id)->update(array( + 'name' => $tag, + )); + } + + /** + * Remove a tag + * + * @access public + * @param integer $tag_id + * @return bool + */ + public function remove($tag_id) + { + return $this->db->table(self::TABLE)->eq('id', $tag_id)->remove(); + } +} diff --git a/sources/app/Model/Task.php b/sources/app/Model/Task.php deleted file mode 100644 index f8b41b9..0000000 --- a/sources/app/Model/Task.php +++ /dev/null @@ -1,223 +0,0 @@ -taskFinder->exists($task_id)) { - return false; - } - - $this->taskFile->removeAll($task_id); - - return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); - } - - /** - * Get a the task id from a text - * - * Example: "Fix bug #1234" will return 1234 - * - * @access public - * @param string $message Text - * @return integer - */ - public function getTaskIdFromText($message) - { - if (preg_match('!#(\d+)!i', $message, $matches) && isset($matches[1])) { - return $matches[1]; - } - - return 0; - } - - /** - * Return the list user selectable recurrence status - * - * @access public - * @return array - */ - public function getRecurrenceStatusList() - { - return array( - Task::RECURRING_STATUS_NONE => t('No'), - Task::RECURRING_STATUS_PENDING => t('Yes'), - ); - } - - /** - * Return the list recurrence triggers - * - * @access public - * @return array - */ - public function getRecurrenceTriggerList() - { - 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'), - ); - } - - /** - * Return the list options to calculate recurrence due date - * - * @access public - * @return array - */ - public function getRecurrenceBasedateList() - { - return array( - Task::RECURRING_BASEDATE_DUEDATE => t('Existing due date'), - Task::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'), - ); - } - - /** - * Return the list recurrence timeframes - * - * @access public - * @return array - */ - public function getRecurrenceTimeframeList() - { - return array( - Task::RECURRING_TIMEFRAME_DAYS => t('Day(s)'), - Task::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'), - Task::RECURRING_TIMEFRAME_YEARS => t('Year(s)'), - ); - } - - /** - * Get task progress based on the column position - * - * @access public - * @param array $task - * @param array $columns - * @return integer - */ - public function getProgress(array $task, array $columns) - { - if ($task['is_active'] == self::STATUS_CLOSED) { - return 100; - } - - $position = 0; - - foreach ($columns as $column_id => $column_title) { - if ($column_id == $task['column_id']) { - break; - } - - $position++; - } - - return round(($position * 100) / count($columns), 1); - } - - /** - * Helper method to duplicate all tasks to another project - * - * @access public - * @param integer $src_project_id - * @param integer $dst_project_id - * @return boolean - */ - public function duplicate($src_project_id, $dst_project_id) - { - $task_ids = $this->taskFinder->getAllIds($src_project_id, array(Task::STATUS_OPEN, Task::STATUS_CLOSED)); - - foreach ($task_ids as $task_id) { - if (! $this->taskDuplication->duplicateToProject($task_id, $dst_project_id)) { - return false; - } - } - - return true; - } -} diff --git a/sources/app/Model/TaskAnalytic.php b/sources/app/Model/TaskAnalyticModel.php similarity index 86% rename from sources/app/Model/TaskAnalytic.php rename to sources/app/Model/TaskAnalyticModel.php index cff5674..3d6fe8a 100644 --- a/sources/app/Model/TaskAnalytic.php +++ b/sources/app/Model/TaskAnalyticModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Task Analytic * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class TaskAnalytic extends Base +class TaskAnalyticModel extends Base { /** * Get the time between date_creation and date_completed or now if empty @@ -48,8 +50,8 @@ class TaskAnalytic extends Base public function getTimeSpentByColumn(array $task) { $result = array(); - $columns = $this->column->getList($task['project_id']); - $sums = $this->transition->getTimeSpentByTask($task['id']); + $columns = $this->columnModel->getList($task['project_id']); + $sums = $this->transitionModel->getTimeSpentByTask($task['id']); foreach ($columns as $column_id => $column_title) { $time_spent = isset($sums[$column_id]) ? $sums[$column_id] : 0; diff --git a/sources/app/Model/TaskCreation.php b/sources/app/Model/TaskCreationModel.php similarity index 57% rename from sources/app/Model/TaskCreation.php rename to sources/app/Model/TaskCreationModel.php index 2d2e550..cd70a02 100644 --- a/sources/app/Model/TaskCreation.php +++ b/sources/app/Model/TaskCreationModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Event\TaskEvent; /** * Task Creation * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class TaskCreation extends Base +class TaskCreationModel extends Base { /** * Create a task @@ -21,18 +22,24 @@ class TaskCreation extends Base */ public function create(array $values) { - if (! $this->project->exists($values['project_id'])) { - return 0; + $position = empty($values['position']) ? 0 : $values['position']; + $tags = array(); + + if (isset($values['tags'])) { + $tags = $values['tags']; + unset($values['tags']); } - $position = empty($values['position']) ? 0 : $values['position']; - $this->prepare($values); - $task_id = $this->persist(Task::TABLE, $values); + $task_id = $this->db->table(TaskModel::TABLE)->persist($values); if ($task_id !== false) { if ($position > 0 && $values['position'] > 1) { - $this->taskPosition->movePosition($values['project_id'], $task_id, $values['column_id'], $position, $values['swimlane_id'], false); + $this->taskPositionModel->movePosition($values['project_id'], $task_id, $values['column_id'], $position, $values['swimlane_id'], false); + } + + if (! empty($tags)) { + $this->taskTagModel->save($values['project_id'], $task_id, $tags); } $this->fireEvents($task_id, $values); @@ -53,14 +60,14 @@ class TaskCreation extends Base $values = $this->dateParser->convert($values, array('date_started'), true); $this->helper->model->removeFields($values, array('another_task')); - $this->helper->model->resetFields($values, array('date_started', 'creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); + $this->helper->model->resetFields($values, array('creator_id', 'owner_id', 'swimlane_id', 'date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); if (empty($values['column_id'])) { - $values['column_id'] = $this->column->getFirstColumnId($values['project_id']); + $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']); } if (empty($values['color_id'])) { - $values['color_id'] = $this->color->getDefaultColor(); + $values['color_id'] = $this->colorModel->getDefaultColor(); } if (empty($values['title'])) { @@ -75,7 +82,7 @@ class TaskCreation extends Base $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; + $values['position'] = $this->taskFinderModel->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1; } /** @@ -89,14 +96,14 @@ class TaskCreation extends Base { $event = new TaskEvent(array('task_id' => $task_id) + $values); - $this->logger->debug('Event fired: '.Task::EVENT_CREATE_UPDATE); - $this->logger->debug('Event fired: '.Task::EVENT_CREATE); + $this->logger->debug('Event fired: '.TaskModel::EVENT_CREATE_UPDATE); + $this->logger->debug('Event fired: '.TaskModel::EVENT_CREATE); - $this->dispatcher->dispatch(Task::EVENT_CREATE_UPDATE, $event); - $this->dispatcher->dispatch(Task::EVENT_CREATE, $event); + $this->dispatcher->dispatch(TaskModel::EVENT_CREATE_UPDATE, $event); + $this->dispatcher->dispatch(TaskModel::EVENT_CREATE, $event); if (! empty($values['description'])) { - $this->userMention->fireEvents($values['description'], Task::EVENT_USER_MENTION, $event); + $this->userMentionModel->fireEvents($values['description'], TaskModel::EVENT_USER_MENTION, $event); } } } diff --git a/sources/app/Model/TaskDuplication.php b/sources/app/Model/TaskDuplication.php deleted file mode 100644 index ebdd4d2..0000000 --- a/sources/app/Model/TaskDuplication.php +++ /dev/null @@ -1,269 +0,0 @@ -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->column->getFirstColumnId($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 - * @param integer $project_id - * @param integer $swimlane_id - * @param integer $column_id - * @param integer $category_id - * @param integer $owner_id - * @return boolean|integer - */ - public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) - { - $values = $this->copyFields($task_id); - $values['project_id'] = $project_id; - $values['column_id'] = $column_id !== null ? $column_id : $values['column_id']; - $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id']; - $values['category_id'] = $category_id !== null ? $category_id : $values['category_id']; - $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id']; - - $this->checkDestinationProjectValues($values); - - return $this->save($task_id, $values); - } - - /** - * Move a task to another project - * - * @access public - * @param integer $task_id - * @param integer $project_id - * @param integer $swimlane_id - * @param integer $column_id - * @param integer $category_id - * @param integer $owner_id - * @return boolean - */ - public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null) - { - $task = $this->taskFinder->getById($task_id); - - $values = array(); - $values['is_active'] = 1; - $values['project_id'] = $project_id; - $values['column_id'] = $column_id !== null ? $column_id : $task['column_id']; - $values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1; - $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id']; - $values['category_id'] = $category_id !== null ? $category_id : $task['category_id']; - $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_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 public - * @param array $values - * @return array - */ - public function checkDestinationProjectValues(array &$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']) - ); - } - - // Check if the column exists for the destination project - if ($values['column_id'] > 0) { - $values['column_id'] = $this->column->getColumnIdByTitle( - $values['project_id'], - $this->column->getColumnTitleById($values['column_id']) - ); - - $values['column_id'] = $values['column_id'] ?: $this->column->getFirstColumnId($values['project_id']); - } - - return $values; - } - - /** - * 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/sources/app/Model/TaskDuplicationModel.php b/sources/app/Model/TaskDuplicationModel.php new file mode 100644 index 0000000..c907965 --- /dev/null +++ b/sources/app/Model/TaskDuplicationModel.php @@ -0,0 +1,145 @@ +save($task_id, $this->copyFields($task_id)); + + if ($new_task_id !== false) { + $this->tagDuplicationModel->duplicateTaskTags($task_id, $new_task_id); + } + + return $new_task_id; + } + + /** + * Check if the assignee and the category are available in the destination project + * + * @access public + * @param array $values + * @return array + */ + public function checkDestinationProjectValues(array &$values) + { + // Check if the assigned user is allowed for the destination project + if ($values['owner_id'] > 0 && ! $this->projectPermissionModel->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->categoryModel->getIdByName( + $values['project_id'], + $this->categoryModel->getNameById($values['category_id']) + ); + } + + // Check if the swimlane exists for the destination project + if ($values['swimlane_id'] > 0) { + $values['swimlane_id'] = $this->swimlaneModel->getIdByName( + $values['project_id'], + $this->swimlaneModel->getNameById($values['swimlane_id']) + ); + } + + // Check if the column exists for the destination project + if ($values['column_id'] > 0) { + $values['column_id'] = $this->columnModel->getColumnIdByTitle( + $values['project_id'], + $this->columnModel->getColumnTitleById($values['column_id']) + ); + + $values['column_id'] = $values['column_id'] ?: $this->columnModel->getFirstColumnId($values['project_id']); + } + + // Check if priority exists for destination project + $values['priority'] = $this->projectTaskPriorityModel->getPriorityForProject( + $values['project_id'], + empty($values['priority']) ? 0 : $values['priority'] + ); + + return $values; + } + + /** + * Duplicate fields for the new task + * + * @access protected + * @param integer $task_id Task id + * @return array + */ + protected function copyFields($task_id) + { + $task = $this->taskFinderModel->getById($task_id); + $values = array(); + + foreach ($this->fieldsToDuplicate as $field) { + $values[$field] = $task[$field]; + } + + return $values; + } + + /** + * Create the new task and duplicate subtasks + * + * @access protected + * @param integer $task_id Task id + * @param array $values Form values + * @return boolean|integer + */ + protected function save($task_id, array $values) + { + $new_task_id = $this->taskCreationModel->create($values); + + if ($new_task_id !== false) { + $this->subtaskModel->duplicate($task_id, $new_task_id); + } + + return $new_task_id; + } +} diff --git a/sources/app/Model/TaskExternalLink.php b/sources/app/Model/TaskExternalLinkModel.php similarity index 85% rename from sources/app/Model/TaskExternalLink.php rename to sources/app/Model/TaskExternalLinkModel.php index f2c756b..220b9c6 100644 --- a/sources/app/Model/TaskExternalLink.php +++ b/sources/app/Model/TaskExternalLinkModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Task External Link Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class TaskExternalLink extends Base +class TaskExternalLinkModel extends Base { /** * SQL table name @@ -29,10 +31,10 @@ class TaskExternalLink extends Base $types = $this->externalLinkManager->getTypes(); $links = $this->db->table(self::TABLE) - ->columns(self::TABLE.'.*', User::TABLE.'.name AS creator_name', User::TABLE.'.username AS creator_username') + ->columns(self::TABLE.'.*', UserModel::TABLE.'.name AS creator_name', UserModel::TABLE.'.username AS creator_username') ->eq('task_id', $task_id) ->asc('title') - ->join(User::TABLE, 'id', 'creator_id') + ->join(UserModel::TABLE, 'id', 'creator_id') ->findAll(); foreach ($links as &$link) { @@ -69,7 +71,7 @@ class TaskExternalLink extends Base $values['date_creation'] = time(); $values['date_modification'] = $values['date_creation']; - return $this->persist(self::TABLE, $values); + return $this->db->table(self::TABLE)->persist($values); } /** diff --git a/sources/app/Model/TaskFile.php b/sources/app/Model/TaskFile.php deleted file mode 100644 index 45a3b97..0000000 --- a/sources/app/Model/TaskFile.php +++ /dev/null @@ -1,54 +0,0 @@ -helper->dt->datetime(time())).'.png'; - return $this->uploadContent($task_id, $original_filename, $blob); - } -} diff --git a/sources/app/Model/TaskFileModel.php b/sources/app/Model/TaskFileModel.php new file mode 100644 index 0000000..7603019 --- /dev/null +++ b/sources/app/Model/TaskFileModel.php @@ -0,0 +1,104 @@ +db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $file_id) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; + } + + /** + * Handle screenshot upload + * + * @access public + * @param integer $task_id Task id + * @param string $blob Base64 encoded image + * @return bool|integer + */ + public function uploadScreenshot($task_id, $blob) + { + $original_filename = e('Screenshot taken %s', $this->helper->dt->datetime(time())).'.png'; + return $this->uploadContent($task_id, $original_filename, $blob); + } +} diff --git a/sources/app/Model/TaskFilter.php b/sources/app/Model/TaskFilter.php deleted file mode 100644 index 1883298..0000000 --- a/sources/app/Model/TaskFilter.php +++ /dev/null @@ -1,745 +0,0 @@ - 'filterByAssignee', - 'T_COLOR' => 'filterByColors', - 'T_DUE' => 'filterByDueDate', - 'T_UPDATED' => 'filterByModificationDate', - 'T_CREATED' => 'filterByCreationDate', - 'T_TITLE' => 'filterByTitle', - 'T_STATUS' => 'filterByStatusName', - 'T_DESCRIPTION' => 'filterByDescription', - 'T_CATEGORY' => 'filterByCategoryName', - 'T_PROJECT' => 'filterByProjectName', - 'T_COLUMN' => 'filterByColumnName', - 'T_REFERENCE' => 'filterByReference', - 'T_SWIMLANE' => 'filterBySwimlaneName', - 'T_LINK' => 'filterByLinkName', - ); - - /** - * Query - * - * @access public - * @var \PicoDb\Table - */ - public $query; - - /** - * Apply filters according to the search input - * - * @access public - * @param string $input - * @return TaskFilter - */ - public function search($input) - { - $tree = $this->lexer->map($this->lexer->tokenize($input)); - $this->query = $this->taskFinder->getExtendedQuery(); - - if (empty($tree)) { - $this->filterByTitle($input); - } - - foreach ($tree as $filter => $value) { - $method = $this->filters[$filter]; - $this->$method($value); - } - - return $this; - } - - /** - * Create a new query - * - * @access public - * @return TaskFilter - */ - public function create() - { - $this->query = $this->db->table(Task::TABLE); - $this->query->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id'); - $this->query->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id'); - - $this->query->columns( - Task::TABLE.'.*', - 'ua.email AS assignee_email', - 'ua.name AS assignee_name', - 'ua.username AS assignee_username', - 'uc.email AS creator_email', - 'uc.username AS creator_username' - ); - - return $this; - } - - /** - * Create a new subtask query - * - * @access public - * @return \PicoDb\Table - */ - public function createSubtaskQuery() - { - return $this->db->table(Subtask::TABLE) - ->columns( - Subtask::TABLE.'.user_id', - Subtask::TABLE.'.task_id', - User::TABLE.'.name', - User::TABLE.'.username' - ) - ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE) - ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE); - } - - /** - * Create a new link query - * - * @access public - * @return \PicoDb\Table - */ - public function createLinkQuery() - { - return $this->db->table(TaskLink::TABLE) - ->columns( - TaskLink::TABLE.'.task_id', - Link::TABLE.'.label' - ) - ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE); - } - - /** - * Clone the filter - * - * @access public - * @return TaskFilter - */ - public function copy() - { - $filter = new static($this->container); - $filter->query = clone($this->query); - $filter->query->condition = clone($this->query->condition); - return $filter; - } - - /** - * Exclude a list of task_id - * - * @access public - * @param integer[] $task_ids - * @return TaskFilter - */ - public function excludeTasks(array $task_ids) - { - $this->query->notin(Task::TABLE.'.id', $task_ids); - return $this; - } - - /** - * Filter by id - * - * @access public - * @param integer $task_id - * @return TaskFilter - */ - public function filterById($task_id) - { - if ($task_id > 0) { - $this->query->eq(Task::TABLE.'.id', $task_id); - } - - return $this; - } - - /** - * Filter by reference - * - * @access public - * @param string $reference - * @return TaskFilter - */ - public function filterByReference($reference) - { - if (! empty($reference)) { - $this->query->eq(Task::TABLE.'.reference', $reference); - } - - return $this; - } - - /** - * Filter by title - * - * @access public - * @param string $title - * @return TaskFilter - */ - public function filterByDescription($title) - { - $this->query->ilike(Task::TABLE.'.description', '%'.$title.'%'); - return $this; - } - - /** - * Filter by title or id if the string is like #123 or an integer - * - * @access public - * @param string $title - * @return TaskFilter - */ - public function filterByTitle($title) - { - if (ctype_digit($title) || (strlen($title) > 1 && $title{0} === '#' && ctype_digit(substr($title, 1)))) { - $this->query->beginOr(); - $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $title)); - $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%'); - $this->query->closeOr(); - } else { - $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%'); - } - - return $this; - } - - /** - * Filter by a list of project id - * - * @access public - * @param array $project_ids - * @return TaskFilter - */ - public function filterByProjects(array $project_ids) - { - $this->query->in(Task::TABLE.'.project_id', $project_ids); - return $this; - } - - /** - * Filter by project id - * - * @access public - * @param integer $project_id - * @return TaskFilter - */ - public function filterByProject($project_id) - { - if ($project_id > 0) { - $this->query->eq(Task::TABLE.'.project_id', $project_id); - } - - return $this; - } - - /** - * Filter by project name - * - * @access public - * @param array $values List of project name - * @return TaskFilter - */ - public function filterByProjectName(array $values) - { - $this->query->beginOr(); - - foreach ($values as $project) { - if (ctype_digit($project)) { - $this->query->eq(Task::TABLE.'.project_id', $project); - } else { - $this->query->ilike(Project::TABLE.'.name', $project); - } - } - - $this->query->closeOr(); - } - - /** - * Filter by swimlane name - * - * @access public - * @param array $values List of swimlane name - * @return TaskFilter - */ - public function filterBySwimlaneName(array $values) - { - $this->query->beginOr(); - - foreach ($values as $swimlane) { - if ($swimlane === 'default') { - $this->query->eq(Task::TABLE.'.swimlane_id', 0); - } else { - $this->query->ilike(Swimlane::TABLE.'.name', $swimlane); - $this->query->addCondition(Task::TABLE.'.swimlane_id=0 AND '.Project::TABLE.'.default_swimlane '.$this->db->getDriver()->getOperator('ILIKE')." '$swimlane'"); - } - } - - $this->query->closeOr(); - } - - /** - * Filter by category id - * - * @access public - * @param integer $category_id - * @return TaskFilter - */ - public function filterByCategory($category_id) - { - if ($category_id >= 0) { - $this->query->eq(Task::TABLE.'.category_id', $category_id); - } - - return $this; - } - - /** - * Filter by category - * - * @access public - * @param array $values List of assignees - * @return TaskFilter - */ - public function filterByCategoryName(array $values) - { - $this->query->beginOr(); - - foreach ($values as $category) { - if ($category === 'none') { - $this->query->eq(Task::TABLE.'.category_id', 0); - } else { - $this->query->eq(Category::TABLE.'.name', $category); - } - } - - $this->query->closeOr(); - } - - /** - * Filter by assignee - * - * @access public - * @param integer $owner_id - * @return TaskFilter - */ - public function filterByOwner($owner_id) - { - if ($owner_id >= 0) { - $this->query->eq(Task::TABLE.'.owner_id', $owner_id); - } - - return $this; - } - - /** - * Filter by assignee names - * - * @access public - * @param array $values List of assignees - * @return TaskFilter - */ - public function filterByAssignee(array $values) - { - $this->query->beginOr(); - - foreach ($values as $assignee) { - switch ($assignee) { - case 'me': - $this->query->eq(Task::TABLE.'.owner_id', $this->userSession->getId()); - break; - case 'nobody': - $this->query->eq(Task::TABLE.'.owner_id', 0); - break; - default: - $this->query->ilike(User::TABLE.'.username', '%'.$assignee.'%'); - $this->query->ilike(User::TABLE.'.name', '%'.$assignee.'%'); - } - } - - $this->filterBySubtaskAssignee($values); - - $this->query->closeOr(); - - return $this; - } - - /** - * Filter by subtask assignee names - * - * @access public - * @param array $values List of assignees - * @return TaskFilter - */ - public function filterBySubtaskAssignee(array $values) - { - $subtaskQuery = $this->createSubtaskQuery(); - $subtaskQuery->beginOr(); - - foreach ($values as $assignee) { - if ($assignee === 'me') { - $subtaskQuery->eq(Subtask::TABLE.'.user_id', $this->userSession->getId()); - } else { - $subtaskQuery->ilike(User::TABLE.'.username', '%'.$assignee.'%'); - $subtaskQuery->ilike(User::TABLE.'.name', '%'.$assignee.'%'); - } - } - - $subtaskQuery->closeOr(); - - $this->query->in(Task::TABLE.'.id', $subtaskQuery->findAllByColumn('task_id')); - - return $this; - } - - /** - * Filter by color - * - * @access public - * @param string $color_id - * @return TaskFilter - */ - public function filterByColor($color_id) - { - if ($color_id !== '') { - $this->query->eq(Task::TABLE.'.color_id', $color_id); - } - - return $this; - } - - /** - * Filter by colors - * - * @access public - * @param array $colors - * @return TaskFilter - */ - public function filterByColors(array $colors) - { - $this->query->beginOr(); - - foreach ($colors as $color) { - $this->filterByColor($this->color->find($color)); - } - - $this->query->closeOr(); - - return $this; - } - - /** - * Filter by column - * - * @access public - * @param integer $column_id - * @return TaskFilter - */ - public function filterByColumn($column_id) - { - if ($column_id >= 0) { - $this->query->eq(Task::TABLE.'.column_id', $column_id); - } - - return $this; - } - - /** - * Filter by column name - * - * @access public - * @param array $values List of column name - * @return TaskFilter - */ - public function filterByColumnName(array $values) - { - $this->query->beginOr(); - - foreach ($values as $project) { - $this->query->ilike(Column::TABLE.'.title', $project); - } - - $this->query->closeOr(); - } - - /** - * Filter by swimlane - * - * @access public - * @param integer $swimlane_id - * @return TaskFilter - */ - public function filterBySwimlane($swimlane_id) - { - if ($swimlane_id >= 0) { - $this->query->eq(Task::TABLE.'.swimlane_id', $swimlane_id); - } - - return $this; - } - - /** - * Filter by status name - * - * @access public - * @param string $status - * @return TaskFilter - */ - public function filterByStatusName($status) - { - if ($status === 'open' || $status === 'closed') { - $this->filterByStatus($status === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED); - } - - return $this; - } - - /** - * Filter by status - * - * @access public - * @param integer $is_active - * @return TaskFilter - */ - public function filterByStatus($is_active) - { - if ($is_active >= 0) { - $this->query->eq(Task::TABLE.'.is_active', $is_active); - } - - return $this; - } - - /** - * Filter by link - * - * @access public - * @param array $values List of links - * @return TaskFilter - */ - public function filterByLinkName(array $values) - { - $this->query->beginOr(); - - $link_query = $this->createLinkQuery()->in(Link::TABLE.'.label', $values); - $matching_task_ids = $link_query->findAllByColumn('task_id'); - if (empty($matching_task_ids)) { - $this->query->eq(Task::TABLE.'.id', 0); - } else { - $this->query->in(Task::TABLE.'.id', $matching_task_ids); - } - - $this->query->closeOr(); - - return $this; - } - - /** - * Filter by due date - * - * @access public - * @param string $date ISO8601 date format - * @return TaskFilter - */ - public function filterByDueDate($date) - { - $this->query->neq(Task::TABLE.'.date_due', 0); - $this->query->notNull(Task::TABLE.'.date_due'); - return $this->filterWithOperator(Task::TABLE.'.date_due', $date, true); - } - - /** - * Filter by due date (range) - * - * @access public - * @param string $start - * @param string $end - * @return TaskFilter - */ - public function filterByDueDateRange($start, $end) - { - $this->query->gte('date_due', $this->dateParser->getTimestampFromIsoFormat($start)); - $this->query->lte('date_due', $this->dateParser->getTimestampFromIsoFormat($end)); - - return $this; - } - - /** - * Filter by start date (range) - * - * @access public - * @param string $start - * @param string $end - * @return TaskFilter - */ - public function filterByStartDateRange($start, $end) - { - $this->query->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'date_started', - 'date_completed' - )); - - return $this; - } - - /** - * Filter by creation date - * - * @access public - * @param string $date ISO8601 date format - * @return TaskFilter - */ - public function filterByCreationDate($date) - { - if ($date === 'recently') { - return $this->filterRecentlyDate(Task::TABLE.'.date_creation'); - } - - return $this->filterWithOperator(Task::TABLE.'.date_creation', $date, true); - } - - /** - * Filter by creation date - * - * @access public - * @param string $start - * @param string $end - * @return TaskFilter - */ - public function filterByCreationDateRange($start, $end) - { - $this->query->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'date_creation', - 'date_completed' - )); - - return $this; - } - - /** - * Filter by modification date - * - * @access public - * @param string $date ISO8601 date format - * @return TaskFilter - */ - public function filterByModificationDate($date) - { - if ($date === 'recently') { - return $this->filterRecentlyDate(Task::TABLE.'.date_modification'); - } - - return $this->filterWithOperator(Task::TABLE.'.date_modification', $date, true); - } - - /** - * Get all results of the filter - * - * @access public - * @return array - */ - public function findAll() - { - return $this->query->asc(Task::TABLE.'.id')->findAll(); - } - - /** - * Get the PicoDb query - * - * @access public - * @return \PicoDb\Table - */ - public function getQuery() - { - return $this->query; - } - - /** - * Get swimlanes and tasks to display the board - * - * @access public - * @return array - */ - public function getBoard($project_id) - { - $tasks = $this->filterByProject($project_id)->query->asc(Task::TABLE.'.position')->findAll(); - - return $this->board->getBoard($project_id, function ($project_id, $column_id, $swimlane_id) use ($tasks) { - return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) { - return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id; - }); - }); - } - - /** - * Filter with an operator - * - * @access public - * @param string $field - * @param string $value - * @param boolean $is_date - * @return TaskFilter - */ - private function filterWithOperator($field, $value, $is_date) - { - $operators = array( - '<=' => 'lte', - '>=' => 'gte', - '<' => 'lt', - '>' => 'gt', - ); - - foreach ($operators as $operator => $method) { - if (strpos($value, $operator) === 0) { - $value = substr($value, strlen($operator)); - $this->query->$method($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value); - return $this; - } - } - - if ($is_date) { - $timestamp = $this->dateParser->getTimestampFromIsoFormat($value); - $this->query->gte($field, $timestamp); - $this->query->lte($field, $timestamp + 86399); - } else { - $this->query->eq($field, $value); - } - - return $this; - } - - /** - * Use the board_highlight_period for the "recently" keyword - * - * @access private - * @param string $field - * @return TaskFilter - */ - private function filterRecentlyDate($field) - { - $duration = $this->config->get('board_highlight_period', 0); - - if ($duration > 0) { - $this->query->gte($field, time() - $duration); - } - - return $this; - } -} diff --git a/sources/app/Model/TaskFinder.php b/sources/app/Model/TaskFinderModel.php similarity index 53% rename from sources/app/Model/TaskFinder.php rename to sources/app/Model/TaskFinderModel.php index 7bca228..0e8585e 100644 --- a/sources/app/Model/TaskFinder.php +++ b/sources/app/Model/TaskFinderModel.php @@ -3,14 +3,15 @@ namespace Kanboard\Model; use PDO; +use Kanboard\Core\Base; /** * Task Finder model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class TaskFinder extends Base +class TaskFinderModel extends Base { /** * Get query for project user overview @@ -27,26 +28,27 @@ class TaskFinder extends Base } return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->columns( - Task::TABLE.'.id', - Task::TABLE.'.title', - Task::TABLE.'.date_due', - Task::TABLE.'.date_started', - Task::TABLE.'.project_id', - Task::TABLE.'.color_id', - Task::TABLE.'.time_spent', - Task::TABLE.'.time_estimated', - Project::TABLE.'.name AS project_name', - Column::TABLE.'.title AS column_name', - User::TABLE.'.username AS assignee_username', - User::TABLE.'.name AS assignee_name' + TaskModel::TABLE.'.id', + TaskModel::TABLE.'.title', + TaskModel::TABLE.'.date_due', + TaskModel::TABLE.'.date_started', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.color_id', + TaskModel::TABLE.'.priority', + TaskModel::TABLE.'.time_spent', + TaskModel::TABLE.'.time_estimated', + ProjectModel::TABLE.'.name AS project_name', + ColumnModel::TABLE.'.title AS column_name', + UserModel::TABLE.'.username AS assignee_username', + UserModel::TABLE.'.name AS assignee_name' ) - ->eq(Task::TABLE.'.is_active', $is_active) - ->in(Project::TABLE.'.id', $project_ids) - ->join(Project::TABLE, 'id', 'project_id') - ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) - ->join(User::TABLE, 'id', 'owner_id', Task::TABLE); + ->eq(TaskModel::TABLE.'.is_active', $is_active) + ->in(ProjectModel::TABLE.'.id', $project_ids) + ->join(ProjectModel::TABLE, 'id', 'project_id') + ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE) + ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE); } /** @@ -59,7 +61,7 @@ class TaskFinder extends Base public function getUserQuery($user_id) { return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->columns( 'tasks.id', 'tasks.title', @@ -67,14 +69,19 @@ class TaskFinder extends Base 'tasks.date_creation', 'tasks.project_id', 'tasks.color_id', + 'tasks.priority', 'tasks.time_spent', 'tasks.time_estimated', - 'projects.name AS project_name' + 'tasks.is_active', + 'tasks.creator_id', + 'projects.name AS project_name', + 'columns.title AS column_title' ) - ->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); + ->join(ProjectModel::TABLE, 'id', 'project_id') + ->join(ColumnModel::TABLE, 'id', 'column_id') + ->eq(TaskModel::TABLE.'.owner_id', $user_id) + ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN) + ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE); } /** @@ -86,15 +93,15 @@ class TaskFinder extends Base public function getExtendedQuery() { return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->columns( - '(SELECT COUNT(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments', - '(SELECT COUNT(*) FROM '.TaskFile::TABLE.' WHERE task_id=tasks.id) AS nb_files', - '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks', - '(SELECT COUNT(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks', - '(SELECT COUNT(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links', - '(SELECT COUNT(*) FROM '.TaskExternalLink::TABLE.' WHERE '.TaskExternalLink::TABLE.'.task_id = tasks.id) AS nb_external_links', - '(SELECT DISTINCT 1 FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id AND '.TaskLink::TABLE.'.link_id = 9) AS is_milestone', + '(SELECT COUNT(*) FROM '.CommentModel::TABLE.' WHERE task_id=tasks.id) AS nb_comments', + '(SELECT COUNT(*) FROM '.TaskFileModel::TABLE.' WHERE task_id=tasks.id) AS nb_files', + '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id) AS nb_subtasks', + '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks', + '(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links', + '(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links', + '(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id AND '.TaskLinkModel::TABLE.'.link_id = 9) AS is_milestone', 'tasks.id', 'tasks.reference', 'tasks.title', @@ -125,43 +132,24 @@ class TaskFinder extends Base 'tasks.recurrence_child', 'tasks.time_estimated', 'tasks.time_spent', - User::TABLE.'.username AS assignee_username', - User::TABLE.'.name AS assignee_name', - User::TABLE.'.email AS assignee_email', - User::TABLE.'.avatar_path AS assignee_avatar_path', - Category::TABLE.'.name AS category_name', - Category::TABLE.'.description AS category_description', - Column::TABLE.'.title AS column_name', - Column::TABLE.'.position AS column_position', - Swimlane::TABLE.'.name AS swimlane_name', - Project::TABLE.'.default_swimlane', - Project::TABLE.'.name AS project_name' + UserModel::TABLE.'.username AS assignee_username', + UserModel::TABLE.'.name AS assignee_name', + UserModel::TABLE.'.email AS assignee_email', + UserModel::TABLE.'.avatar_path AS assignee_avatar_path', + CategoryModel::TABLE.'.name AS category_name', + CategoryModel::TABLE.'.description AS category_description', + ColumnModel::TABLE.'.title AS column_name', + ColumnModel::TABLE.'.position AS column_position', + SwimlaneModel::TABLE.'.name AS swimlane_name', + ProjectModel::TABLE.'.default_swimlane', + ProjectModel::TABLE.'.name AS project_name' ) - ->join(User::TABLE, 'id', 'owner_id', Task::TABLE) - ->join(Category::TABLE, 'id', 'category_id', Task::TABLE) - ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) - ->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE) - ->join(Project::TABLE, 'id', 'project_id', Task::TABLE); - } - - /** - * Get all tasks shown on the board (sorted by position) - * - * @access public - * @param integer $project_id Project id - * @param integer $column_id Column id - * @param integer $swimlane_id Swimlane id - * @return array - */ - public function getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id = 0) - { - return $this->getExtendedQuery() - ->eq(Task::TABLE.'.project_id', $project_id) - ->eq(Task::TABLE.'.column_id', $column_id) - ->eq(Task::TABLE.'.swimlane_id', $swimlane_id) - ->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN) - ->asc(Task::TABLE.'.position') - ->findAll(); + ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE) + ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id') + ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE) + ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE) + ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE) + ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE); } /** @@ -172,12 +160,13 @@ class TaskFinder extends Base * @param integer $status_id Status id * @return array */ - public function getAll($project_id, $status_id = Task::STATUS_OPEN) + public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN) { return $this->db - ->table(Task::TABLE) - ->eq(Task::TABLE.'.project_id', $project_id) - ->eq(Task::TABLE.'.is_active', $status_id) + ->table(TaskModel::TABLE) + ->eq(TaskModel::TABLE.'.project_id', $project_id) + ->eq(TaskModel::TABLE.'.is_active', $status_id) + ->asc(TaskModel::TABLE.'.id') ->findAll(); } @@ -189,13 +178,14 @@ class TaskFinder extends Base * @param array $status * @return array */ - public function getAllIds($project_id, array $status = array(Task::STATUS_OPEN)) + public function getAllIds($project_id, array $status = array(TaskModel::STATUS_OPEN)) { return $this->db - ->table(Task::TABLE) - ->eq(Task::TABLE.'.project_id', $project_id) - ->in(Task::TABLE.'.is_active', $status) - ->findAllByColumn('id'); + ->table(TaskModel::TABLE) + ->eq(TaskModel::TABLE.'.project_id', $project_id) + ->in(TaskModel::TABLE.'.is_active', $status) + ->asc(TaskModel::TABLE.'.id') + ->findAllByColumn(TaskModel::TABLE.'.id'); } /** @@ -206,24 +196,24 @@ class TaskFinder extends Base */ public function getOverdueTasksQuery() { - return $this->db->table(Task::TABLE) + return $this->db->table(TaskModel::TABLE) ->columns( - Task::TABLE.'.id', - 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' + TaskModel::TABLE.'.id', + TaskModel::TABLE.'.title', + TaskModel::TABLE.'.date_due', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.creator_id', + TaskModel::TABLE.'.owner_id', + ProjectModel::TABLE.'.name AS project_name', + UserModel::TABLE.'.username AS assignee_username', + UserModel::TABLE.'.name AS assignee_name' ) - ->join(Project::TABLE, 'id', 'project_id') - ->join(User::TABLE, 'id', 'owner_id') - ->eq(Project::TABLE.'.is_active', 1) - ->eq(Task::TABLE.'.is_active', 1) - ->neq(Task::TABLE.'.date_due', 0) - ->lte(Task::TABLE.'.date_due', mktime(23, 59, 59)); + ->join(ProjectModel::TABLE, 'id', 'project_id') + ->join(UserModel::TABLE, 'id', 'owner_id') + ->eq(ProjectModel::TABLE.'.is_active', 1) + ->eq(TaskModel::TABLE.'.is_active', 1) + ->neq(TaskModel::TABLE.'.date_due', 0) + ->lte(TaskModel::TABLE.'.date_due', mktime(23, 59, 59)); } /** @@ -246,7 +236,7 @@ class TaskFinder extends Base */ public function getOverdueTasksByProject($project_id) { - return $this->getOverdueTasksQuery()->eq(Task::TABLE.'.project_id', $project_id)->findAll(); + return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.project_id', $project_id)->findAll(); } /** @@ -258,7 +248,7 @@ class TaskFinder extends Base */ public function getOverdueTasksByUser($user_id) { - return $this->getOverdueTasksQuery()->eq(Task::TABLE.'.owner_id', $user_id)->findAll(); + return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.owner_id', $user_id)->findAll(); } /** @@ -270,7 +260,7 @@ class TaskFinder extends Base */ public function getProjectId($task_id) { - return (int) $this->db->table(Task::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0; + return (int) $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0; } /** @@ -282,7 +272,7 @@ class TaskFinder extends Base */ public function getById($task_id) { - return $this->db->table(Task::TABLE)->eq('id', $task_id)->findOne(); + return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOne(); } /** @@ -295,7 +285,7 @@ class TaskFinder extends Base */ public function getByReference($project_id, $reference) { - return $this->db->table(Task::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne(); + return $this->db->table(TaskModel::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne(); } /** @@ -362,6 +352,28 @@ class TaskFinder extends Base return $rq->fetch(PDO::FETCH_ASSOC); } + /** + * Get iCal query + * + * @access public + * @return \PicoDb\Table + */ + public function getICalQuery() + { + return $this->db->table(TaskModel::TABLE) + ->left(UserModel::TABLE, 'ua', 'id', TaskModel::TABLE, 'owner_id') + ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id') + ->columns( + TaskModel::TABLE.'.*', + 'ua.email AS assignee_email', + 'ua.name AS assignee_name', + 'ua.username AS assignee_username', + 'uc.email AS creator_email', + 'uc.name AS creator_name', + 'uc.username AS creator_username' + ); + } + /** * Count all tasks for a given project and status * @@ -370,10 +382,10 @@ class TaskFinder extends Base * @param array $status List of status id * @return integer */ - public function countByProjectId($project_id, array $status = array(Task::STATUS_OPEN, Task::STATUS_CLOSED)) + public function countByProjectId($project_id, array $status = array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED)) { return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->eq('project_id', $project_id) ->in('is_active', $status) ->count(); @@ -390,7 +402,7 @@ class TaskFinder extends Base public function countByColumnId($project_id, $column_id) { return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->eq('project_id', $project_id) ->eq('column_id', $column_id) ->eq('is_active', 1) @@ -409,7 +421,7 @@ class TaskFinder extends Base public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id) { return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->eq('project_id', $project_id) ->eq('column_id', $column_id) ->eq('swimlane_id', $swimlane_id) @@ -426,6 +438,22 @@ class TaskFinder extends Base */ public function exists($task_id) { - return $this->db->table(Task::TABLE)->eq('id', $task_id)->exists(); + return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->exists(); + } + + /** + * Get project token + * + * @access public + * @param integer $task_id + * @return string + */ + public function getProjectToken($task_id) + { + return $this->db + ->table(TaskModel::TABLE) + ->eq(TaskModel::TABLE.'.id', $task_id) + ->join(ProjectModel::TABLE, 'id', 'project_id') + ->findOneColumn(ProjectModel::TABLE.'.token'); } } diff --git a/sources/app/Model/TaskLink.php b/sources/app/Model/TaskLinkModel.php similarity index 70% rename from sources/app/Model/TaskLink.php rename to sources/app/Model/TaskLinkModel.php index e46ea47..09978ea 100644 --- a/sources/app/Model/TaskLink.php +++ b/sources/app/Model/TaskLinkModel.php @@ -2,16 +2,17 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Event\TaskLinkEvent; /** * TaskLink model * - * @package model + * @package Kanboard\Model * @author Olivier Maridat * @author Frederic Guillot */ -class TaskLink extends Base +class TaskLinkModel extends Base { /** * SQL table name @@ -27,6 +28,22 @@ class TaskLink extends Base */ const EVENT_CREATE_UPDATE = 'tasklink.create_update'; + /** + * Get projectId from $task_link_id + * + * @access public + * @param integer $task_link_id + * @return integer + */ + public function getProjectId($task_link_id) + { + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $task_link_id) + ->join(TaskModel::TABLE, 'id', 'task_id') + ->findOneColumn(TaskModel::TABLE . '.project_id') ?: 0; + } + /** * Get a task link * @@ -48,7 +65,7 @@ class TaskLink extends Base */ public function getOppositeTaskLink(array $task_link) { - $opposite_link_id = $this->link->getOppositeLinkId($task_link['link_id']); + $opposite_link_id = $this->linkModel->getOppositeLinkId($task_link['link_id']); return $this->db->table(self::TABLE) ->eq('opposite_task_id', $task_link['task_id']) @@ -71,31 +88,31 @@ class TaskLink extends Base ->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.'.column_id', - Task::TABLE.'.color_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', - Column::TABLE.'.title AS column_title', - Project::TABLE.'.name AS project_name' + LinkModel::TABLE.'.label', + TaskModel::TABLE.'.title', + TaskModel::TABLE.'.is_active', + TaskModel::TABLE.'.project_id', + TaskModel::TABLE.'.column_id', + TaskModel::TABLE.'.color_id', + TaskModel::TABLE.'.time_spent AS task_time_spent', + TaskModel::TABLE.'.time_estimated AS task_time_estimated', + TaskModel::TABLE.'.owner_id AS task_assignee_id', + UserModel::TABLE.'.username AS task_assignee_username', + UserModel::TABLE.'.name AS task_assignee_name', + ColumnModel::TABLE.'.title AS column_title', + ProjectModel::TABLE.'.name AS project_name' ) ->eq(self::TABLE.'.task_id', $task_id) - ->join(Link::TABLE, 'id', 'link_id') - ->join(Task::TABLE, 'id', 'opposite_task_id') - ->join(Column::TABLE, 'id', 'column_id', Task::TABLE) - ->join(User::TABLE, 'id', 'owner_id', Task::TABLE) - ->join(Project::TABLE, 'id', 'project_id', Task::TABLE) - ->asc(Link::TABLE.'.id') - ->desc(Column::TABLE.'.position') - ->desc(Task::TABLE.'.is_active') - ->asc(Task::TABLE.'.position') - ->asc(Task::TABLE.'.id') + ->join(LinkModel::TABLE, 'id', 'link_id') + ->join(TaskModel::TABLE, 'id', 'opposite_task_id') + ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE) + ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE) + ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE) + ->asc(LinkModel::TABLE.'.id') + ->desc(ColumnModel::TABLE.'.position') + ->desc(TaskModel::TABLE.'.is_active') + ->asc(TaskModel::TABLE.'.position') + ->asc(TaskModel::TABLE.'.id') ->findAll(); } @@ -131,7 +148,7 @@ class TaskLink extends Base private function fireEvents(array $events) { foreach ($events as $event) { - $event['project_id'] = $this->taskFinder->getProjectId($event['task_id']); + $event['project_id'] = $this->taskFinderModel->getProjectId($event['task_id']); $this->container['dispatcher']->dispatch(self::EVENT_CREATE_UPDATE, new TaskLinkEvent($event)); } } @@ -151,7 +168,7 @@ class TaskLink extends Base $this->db->startTransaction(); // Get opposite link - $opposite_link_id = $this->link->getOppositeLinkId($link_id); + $opposite_link_id = $this->linkModel->getOppositeLinkId($link_id); $values = array( 'task_id' => $task_id, @@ -203,7 +220,7 @@ class TaskLink extends Base $opposite_task_link = $this->getOppositeTaskLink($task_link); // Get opposite link - $opposite_link_id = $this->link->getOppositeLinkId($link_id); + $opposite_link_id = $this->linkModel->getOppositeLinkId($link_id); // Update the original task link $values = array( @@ -247,7 +264,7 @@ class TaskLink extends Base $this->db->startTransaction(); $link = $this->getById($task_link_id); - $link_id = $this->link->getOppositeLinkId($link['link_id']); + $link_id = $this->linkModel->getOppositeLinkId($link['link_id']); $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove(); diff --git a/sources/app/Model/TaskMetadata.php b/sources/app/Model/TaskMetadataModel.php similarity index 55% rename from sources/app/Model/TaskMetadata.php rename to sources/app/Model/TaskMetadataModel.php index 1fd1841..dc3f56e 100644 --- a/sources/app/Model/TaskMetadata.php +++ b/sources/app/Model/TaskMetadataModel.php @@ -5,17 +5,22 @@ namespace Kanboard\Model; /** * Task Metadata * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class TaskMetadata extends Metadata +class TaskMetadataModel extends MetadataModel { /** - * SQL table name + * Get the table * - * @var string + * @abstract + * @access protected + * @return string */ - const TABLE = 'task_has_metadata'; + protected function getTable() + { + return 'task_has_metadata'; + } /** * Define the entity key diff --git a/sources/app/Model/TaskModel.php b/sources/app/Model/TaskModel.php new file mode 100644 index 0000000..5cddb50 --- /dev/null +++ b/sources/app/Model/TaskModel.php @@ -0,0 +1,146 @@ +taskFinderModel->exists($task_id)) { + return false; + } + + $this->taskFileModel->removeAll($task_id); + + return $this->db->table(self::TABLE)->eq('id', $task_id)->remove(); + } + + /** + * Get a the task id from a text + * + * Example: "Fix bug #1234" will return 1234 + * + * @access public + * @param string $message Text + * @return integer + */ + public function getTaskIdFromText($message) + { + if (preg_match('!#(\d+)!i', $message, $matches) && isset($matches[1])) { + return $matches[1]; + } + + return 0; + } + + /** + * Get task progress based on the column position + * + * @access public + * @param array $task + * @param array $columns + * @return integer + */ + public function getProgress(array $task, array $columns) + { + if ($task['is_active'] == self::STATUS_CLOSED) { + return 100; + } + + $position = 0; + + foreach ($columns as $column_id => $column_title) { + if ($column_id == $task['column_id']) { + break; + } + + $position++; + } + + return round(($position * 100) / count($columns), 1); + } +} diff --git a/sources/app/Model/TaskModification.php b/sources/app/Model/TaskModificationModel.php similarity index 70% rename from sources/app/Model/TaskModification.php rename to sources/app/Model/TaskModificationModel.php index a77b78a..be5f53c 100644 --- a/sources/app/Model/TaskModification.php +++ b/sources/app/Model/TaskModificationModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Event\TaskEvent; /** * Task Modification * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class TaskModification extends Base +class TaskModificationModel extends Base { /** * Update a task @@ -22,10 +23,11 @@ class TaskModification extends Base */ public function update(array $values, $fire_events = true) { - $original_task = $this->taskFinder->getById($values['id']); + $original_task = $this->taskFinderModel->getById($values['id']); + $this->updateTags($values, $original_task); $this->prepare($values); - $result = $this->db->table(Task::TABLE)->eq('id', $original_task['id'])->update($values); + $result = $this->db->table(TaskModel::TABLE)->eq('id', $original_task['id'])->update($values); if ($fire_events && $result) { $this->fireEvents($original_task, $values); @@ -51,10 +53,10 @@ class TaskModification extends Base unset($event_data['changes']['date_modification']); if ($this->isFieldModified('owner_id', $event_data['changes'])) { - $events[] = Task::EVENT_ASSIGNEE_CHANGE; + $events[] = TaskModel::EVENT_ASSIGNEE_CHANGE; } elseif (! empty($event_data['changes'])) { - $events[] = Task::EVENT_CREATE_UPDATE; - $events[] = Task::EVENT_UPDATE; + $events[] = TaskModel::EVENT_CREATE_UPDATE; + $events[] = TaskModel::EVENT_UPDATE; } foreach ($events as $event) { @@ -79,10 +81,10 @@ class TaskModification extends Base /** * Prepare data before task modification * - * @access public - * @param array $values Form values + * @access protected + * @param array $values */ - public function prepare(array &$values) + protected function prepare(array &$values) { $values = $this->dateParser->convert($values, array('date_due')); $values = $this->dateParser->convert($values, array('date_started'), true); @@ -93,4 +95,19 @@ class TaskModification extends Base $values['date_modification'] = time(); } + + /** + * Update tags + * + * @access protected + * @param array $values + * @param array $original_task + */ + protected function updateTags(array &$values, array $original_task) + { + if (isset($values['tags'])) { + $this->taskTagModel->save($original_task['project_id'], $values['id'], $values['tags']); + unset($values['tags']); + } + } } diff --git a/sources/app/Model/TaskPermission.php b/sources/app/Model/TaskPermission.php deleted file mode 100644 index b1e0258..0000000 --- a/sources/app/Model/TaskPermission.php +++ /dev/null @@ -1,34 +0,0 @@ -userSession->isAdmin() || $this->projectUserRole->getUserRole($task['project_id'], $this->userSession->getId()) === Role::PROJECT_MANAGER) { - return true; - } elseif (isset($task['creator_id']) && $task['creator_id'] == $this->userSession->getId()) { - return true; - } - - return false; - } -} diff --git a/sources/app/Model/TaskPosition.php b/sources/app/Model/TaskPositionModel.php similarity index 87% rename from sources/app/Model/TaskPosition.php rename to sources/app/Model/TaskPositionModel.php index 4c9928d..9fdb8f7 100644 --- a/sources/app/Model/TaskPosition.php +++ b/sources/app/Model/TaskPositionModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Event\TaskEvent; /** * Task Position * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class TaskPosition extends Base +class TaskPositionModel extends Base { /** * Move a task to another column or to another position @@ -30,9 +31,9 @@ class TaskPosition extends Base return false; } - $task = $this->taskFinder->getById($task_id); + $task = $this->taskFinderModel->getById($task_id); - if ($task['is_active'] == Task::STATUS_CLOSED) { + if ($task['is_active'] == TaskModel::STATUS_CLOSED) { return true; } @@ -131,7 +132,7 @@ class TaskPosition extends Base */ private function saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id) { - $tasks_ids = $this->db->table(Task::TABLE) + $tasks_ids = $this->db->table(TaskModel::TABLE) ->eq('is_active', 1) ->eq('swimlane_id', $swimlane_id) ->eq('project_id', $project_id) @@ -168,7 +169,7 @@ class TaskPosition extends Base $now = time(); - return $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array( + return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array( 'date_moved' => $now, 'date_modification' => $now, )); @@ -186,7 +187,7 @@ class TaskPosition extends Base */ private function saveTaskPosition($task_id, $position, $column_id, $swimlane_id) { - $result = $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array( + $result = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array( 'position' => $position, 'column_id' => $column_id, 'swimlane_id' => $swimlane_id, @@ -225,14 +226,14 @@ class TaskPosition extends Base ); if ($task['swimlane_id'] != $new_swimlane_id) { - $this->logger->debug('Event fired: '.Task::EVENT_MOVE_SWIMLANE); - $this->dispatcher->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_SWIMLANE); + $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); } elseif ($task['column_id'] != $new_column_id) { - $this->logger->debug('Event fired: '.Task::EVENT_MOVE_COLUMN); - $this->dispatcher->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_COLUMN); + $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); } elseif ($task['position'] != $new_position) { - $this->logger->debug('Event fired: '.Task::EVENT_MOVE_POSITION); - $this->dispatcher->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data)); + $this->logger->debug('Event fired: '.TaskModel::EVENT_MOVE_POSITION); + $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_POSITION, new TaskEvent($event_data)); } } } diff --git a/sources/app/Model/TaskProjectDuplicationModel.php b/sources/app/Model/TaskProjectDuplicationModel.php new file mode 100644 index 0000000..8ebed25 --- /dev/null +++ b/sources/app/Model/TaskProjectDuplicationModel.php @@ -0,0 +1,60 @@ +prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id); + $this->checkDestinationProjectValues($values); + $new_task_id = $this->save($task_id, $values); + + if ($new_task_id !== false) { + $this->tagDuplicationModel->duplicateTaskTagsToAnotherProject($task_id, $new_task_id, $project_id); + } + + return $new_task_id; + } + + /** + * Prepare values before duplication + * + * @access protected + * @param integer $task_id + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $category_id + * @param integer $owner_id + * @return array + */ + protected function prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id) + { + $values = $this->copyFields($task_id); + $values['project_id'] = $project_id; + $values['column_id'] = $column_id !== null ? $column_id : $values['column_id']; + $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id']; + $values['category_id'] = $category_id !== null ? $category_id : $values['category_id']; + $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id']; + return $values; + } +} diff --git a/sources/app/Model/TaskProjectMoveModel.php b/sources/app/Model/TaskProjectMoveModel.php new file mode 100644 index 0000000..eda23c0 --- /dev/null +++ b/sources/app/Model/TaskProjectMoveModel.php @@ -0,0 +1,68 @@ +taskFinderModel->getById($task_id); + $values = $this->prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, $task); + + $this->checkDestinationProjectValues($values); + $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id); + + if ($this->db->table(TaskModel::TABLE)->eq('id', $task['id'])->update($values)) { + $event = new TaskEvent(array_merge($task, $values, array('task_id' => $task['id']))); + $this->dispatcher->dispatch(TaskModel::EVENT_MOVE_PROJECT, $event); + } + + return true; + } + + /** + * Prepare new task values + * + * @access protected + * @param integer $project_id + * @param integer $swimlane_id + * @param integer $column_id + * @param integer $category_id + * @param integer $owner_id + * @param array $task + * @return array + */ + protected function prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, array $task) + { + $values = array(); + $values['is_active'] = 1; + $values['project_id'] = $project_id; + $values['column_id'] = $column_id !== null ? $column_id : $task['column_id']; + $values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1; + $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id']; + $values['category_id'] = $category_id !== null ? $category_id : $task['category_id']; + $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id']; + $values['priority'] = $task['priority']; + return $values; + } +} diff --git a/sources/app/Model/TaskRecurrenceModel.php b/sources/app/Model/TaskRecurrenceModel.php new file mode 100644 index 0000000..ffe43f8 --- /dev/null +++ b/sources/app/Model/TaskRecurrenceModel.php @@ -0,0 +1,147 @@ + t('No'), + TaskModel::RECURRING_STATUS_PENDING => t('Yes'), + ); + } + + /** + * Return the list recurrence triggers + * + * @access public + * @return array + */ + public function getRecurrenceTriggerList() + { + return array( + TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'), + TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'), + TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'), + ); + } + + /** + * Return the list options to calculate recurrence due date + * + * @access public + * @return array + */ + public function getRecurrenceBasedateList() + { + return array( + TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'), + TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'), + ); + } + + /** + * Return the list recurrence timeframes + * + * @access public + * @return array + */ + public function getRecurrenceTimeframeList() + { + return array( + TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'), + TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'), + TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'), + ); + } + + /** + * 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'] == TaskModel::RECURRING_STATUS_PENDING) { + $values['recurrence_parent'] = $task_id; + $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']); + $this->calculateRecurringTaskDueDate($values); + + $recurring_task_id = $this->save($task_id, $values); + + if ($recurring_task_id !== false) { + $this->tagDuplicationModel->duplicateTaskTags($task_id, $recurring_task_id); + + $parent_update = $this->db + ->table(TaskModel::TABLE) + ->eq('id', $task_id) + ->update(array( + 'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED, + 'recurrence_child' => $recurring_task_id, + )); + + if ($parent_update) { + return $recurring_task_id; + } + } + } + + return false; + } + + /** + * 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'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) { + $values['date_due'] = time(); + } + + $factor = abs($values['recurrence_factor']); + $subtract = $values['recurrence_factor'] < 0; + + switch ($values['recurrence_timeframe']) { + case TaskModel::RECURRING_TIMEFRAME_MONTHS: + $interval = 'P' . $factor . 'M'; + break; + case TaskModel::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(); + } + } +} diff --git a/sources/app/Model/TaskStatus.php b/sources/app/Model/TaskStatusModel.php similarity index 74% rename from sources/app/Model/TaskStatus.php rename to sources/app/Model/TaskStatusModel.php index 2b90281..4d573f0 100644 --- a/sources/app/Model/TaskStatus.php +++ b/sources/app/Model/TaskStatusModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Event\TaskEvent; /** * Task Status * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class TaskStatus extends Base +class TaskStatusModel extends Base { /** * Return true if the task is closed @@ -21,7 +22,7 @@ class TaskStatus extends Base */ public function isClosed($task_id) { - return $this->checkStatus($task_id, Task::STATUS_CLOSED); + return $this->checkStatus($task_id, TaskModel::STATUS_CLOSED); } /** @@ -33,7 +34,7 @@ class TaskStatus extends Base */ public function isOpen($task_id) { - return $this->checkStatus($task_id, Task::STATUS_OPEN); + return $this->checkStatus($task_id, TaskModel::STATUS_OPEN); } /** @@ -45,8 +46,8 @@ class TaskStatus extends Base */ public function close($task_id) { - $this->subtask->closeAll($task_id); - return $this->changeStatus($task_id, Task::STATUS_CLOSED, time(), Task::EVENT_CLOSE); + $this->subtaskModel->closeAll($task_id); + return $this->changeStatus($task_id, TaskModel::STATUS_CLOSED, time(), TaskModel::EVENT_CLOSE); } /** @@ -58,7 +59,7 @@ class TaskStatus extends Base */ public function open($task_id) { - return $this->changeStatus($task_id, Task::STATUS_OPEN, 0, Task::EVENT_OPEN); + return $this->changeStatus($task_id, TaskModel::STATUS_OPEN, 0, TaskModel::EVENT_OPEN); } /** @@ -83,7 +84,13 @@ class TaskStatus extends Base */ public function closeTasksBySwimlaneAndColumn($swimlane_id, $column_id) { - $task_ids = $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->eq('column_id', $column_id)->findAllByColumn('id'); + $task_ids = $this->db + ->table(TaskModel::TABLE) + ->eq('swimlane_id', $swimlane_id) + ->eq('column_id', $column_id) + ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN) + ->findAllByColumn('id'); + $this->closeMultipleTasks($task_ids); } @@ -99,12 +106,12 @@ class TaskStatus extends Base */ private function changeStatus($task_id, $status, $date_completed, $event) { - if (! $this->taskFinder->exists($task_id)) { + if (! $this->taskFinderModel->exists($task_id)) { return false; } $result = $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->eq('id', $task_id) ->update(array( 'is_active' => $status, @@ -114,7 +121,7 @@ class TaskStatus extends Base if ($result) { $this->logger->debug('Event fired: '.$event); - $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id))); + $this->dispatcher->dispatch($event, new TaskEvent(array('task_id' => $task_id) + $this->taskFinderModel->getById($task_id))); } return $result; @@ -131,7 +138,7 @@ class TaskStatus extends Base private function checkStatus($task_id, $status) { return $this->db - ->table(Task::TABLE) + ->table(TaskModel::TABLE) ->eq('id', $task_id) ->eq('is_active', $status) ->count() === 1; diff --git a/sources/app/Model/TaskTagModel.php b/sources/app/Model/TaskTagModel.php new file mode 100644 index 0000000..0553cc6 --- /dev/null +++ b/sources/app/Model/TaskTagModel.php @@ -0,0 +1,184 @@ +db->table(TagModel::TABLE) + ->eq(self::TABLE.'.task_id', $task_id) + ->notIn(TagModel::TABLE.'.project_id', array(0, $project_id)) + ->join(self::TABLE, 'tag_id', 'id') + ->findAllByColumn(TagModel::TABLE.'.id'); + } + + /** + * Get all tags associated to a task + * + * @access public + * @param integer $task_id + * @return array + */ + public function getTagsByTask($task_id) + { + return $this->db->table(TagModel::TABLE) + ->columns(TagModel::TABLE.'.id', TagModel::TABLE.'.name') + ->eq(self::TABLE.'.task_id', $task_id) + ->join(self::TABLE, 'tag_id', 'id') + ->findAll(); + } + + /** + * Get all tags associated to a list of tasks + * + * @access public + * @param integer[] $task_ids + * @return array + */ + public function getTagsByTasks($task_ids) + { + if (empty($task_ids)) { + return array(); + } + + $tags = $this->db->table(TagModel::TABLE) + ->columns(TagModel::TABLE.'.id', TagModel::TABLE.'.name', self::TABLE.'.task_id') + ->in(self::TABLE.'.task_id', $task_ids) + ->join(self::TABLE, 'tag_id', 'id') + ->findAll(); + + return array_column_index($tags, 'task_id'); + } + + /** + * Get dictionary of tags + * + * @access public + * @param integer $task_id + * @return array + */ + public function getList($task_id) + { + $tags = $this->getTagsByTask($task_id); + return array_column($tags, 'name', 'id'); + } + + /** + * Add or update a list of tags to a task + * + * @access public + * @param integer $project_id + * @param integer $task_id + * @param string[] $tags + * @return boolean + */ + public function save($project_id, $task_id, array $tags) + { + $task_tags = $this->getList($task_id); + $tags = array_filter($tags); + + return $this->associateTags($project_id, $task_id, $task_tags, $tags) && + $this->dissociateTags($task_id, $task_tags, $tags); + } + + /** + * Associate a tag to a task + * + * @access public + * @param integer $task_id + * @param integer $tag_id + * @return boolean + */ + public function associateTag($task_id, $tag_id) + { + return $this->db->table(self::TABLE)->insert(array( + 'task_id' => $task_id, + 'tag_id' => $tag_id, + )); + } + + /** + * Dissociate a tag from a task + * + * @access public + * @param integer $task_id + * @param integer $tag_id + * @return boolean + */ + public function dissociateTag($task_id, $tag_id) + { + return $this->db->table(self::TABLE) + ->eq('task_id', $task_id) + ->eq('tag_id', $tag_id) + ->remove(); + } + + /** + * Associate missing tags + * + * @access protected + * @param integer $project_id + * @param integer $task_id + * @param array $task_tags + * @param string[] $tags + * @return bool + */ + protected function associateTags($project_id, $task_id, $task_tags, $tags) + { + foreach ($tags as $tag) { + $tag_id = $this->tagModel->findOrCreateTag($project_id, $tag); + + if (! isset($task_tags[$tag_id]) && ! $this->associateTag($task_id, $tag_id)) { + return false; + } + } + + return true; + } + + /** + * Dissociate removed tags + * + * @access protected + * @param integer $task_id + * @param array $task_tags + * @param string[] $tags + * @return bool + */ + protected function dissociateTags($task_id, $task_tags, $tags) + { + foreach ($task_tags as $tag_id => $tag) { + if (! in_array($tag, $tags)) { + if (! $this->dissociateTag($task_id, $tag_id)) { + return false; + } + } + } + + return true; + } +} diff --git a/sources/app/Model/TimezoneModel.php b/sources/app/Model/TimezoneModel.php new file mode 100644 index 0000000..8b3e895 --- /dev/null +++ b/sources/app/Model/TimezoneModel.php @@ -0,0 +1,58 @@ + t('Application default')) + $listing; + } + + return $listing; + } + + /** + * Get current timezone + * + * @access public + * @return string + */ + public function getCurrentTimezone() + { + if ($this->userSession->isLogged() && ! empty($this->sessionStorage->user['timezone'])) { + return $this->sessionStorage->user['timezone']; + } + + return $this->configModel->get('application_timezone', 'UTC'); + } + + /** + * Set timezone + * + * @access public + */ + public function setCurrentTimezone() + { + date_default_timezone_set($this->getCurrentTimezone()); + } +} diff --git a/sources/app/Model/Transition.php b/sources/app/Model/TransitionModel.php similarity index 76% rename from sources/app/Model/Transition.php rename to sources/app/Model/TransitionModel.php index 870d95f..a4a5847 100644 --- a/sources/app/Model/Transition.php +++ b/sources/app/Model/TransitionModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * Transition * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class Transition extends Base +class TransitionModel extends Base { /** * SQL table name @@ -69,17 +71,17 @@ class Transition extends Base ->columns( 'src.title as src_column', 'dst.title as dst_column', - User::TABLE.'.name', - User::TABLE.'.username', + UserModel::TABLE.'.name', + UserModel::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(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') - ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->join(UserModel::TABLE, 'id', 'user_id') + ->join(ColumnModel::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(ColumnModel::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') ->findAll(); } @@ -104,12 +106,12 @@ class Transition extends Base return $this->db->table(self::TABLE) ->columns( - Task::TABLE.'.id', - Task::TABLE.'.title', + TaskModel::TABLE.'.id', + TaskModel::TABLE.'.title', 'src.title as src_column', 'dst.title as dst_column', - User::TABLE.'.name', - User::TABLE.'.username', + UserModel::TABLE.'.name', + UserModel::TABLE.'.username', self::TABLE.'.user_id', self::TABLE.'.date', self::TABLE.'.time_spent' @@ -119,10 +121,10 @@ class Transition extends Base ->eq(self::TABLE.'.project_id', $project_id) ->desc('date') ->desc(self::TABLE.'.id') - ->join(Task::TABLE, 'id', 'task_id') - ->join(User::TABLE, 'id', 'user_id') - ->join(Column::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') - ->join(Column::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->join(TaskModel::TABLE, 'id', 'task_id') + ->join(UserModel::TABLE, 'id', 'user_id') + ->join(ColumnModel::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(ColumnModel::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') ->findAll(); } } diff --git a/sources/app/Model/UserFilter.php b/sources/app/Model/UserFilter.php deleted file mode 100644 index ff546e9..0000000 --- a/sources/app/Model/UserFilter.php +++ /dev/null @@ -1,80 +0,0 @@ -query = $this->db->table(User::TABLE); - $this->input = $input; - return $this; - } - - /** - * Filter users by name or username - * - * @access public - * @return UserFilter - */ - public function filterByUsernameOrByName() - { - $this->query->beginOr() - ->ilike('username', '%'.$this->input.'%') - ->ilike('name', '%'.$this->input.'%') - ->closeOr(); - - return $this; - } - - /** - * Get all results of the filter - * - * @access public - * @return array - */ - public function findAll() - { - return $this->query->findAll(); - } - - /** - * Get the PicoDb query - * - * @access public - * @return \PicoDb\Table - */ - public function getQuery() - { - return $this->query; - } -} diff --git a/sources/app/Model/UserLocking.php b/sources/app/Model/UserLockingModel.php similarity index 86% rename from sources/app/Model/UserLocking.php rename to sources/app/Model/UserLockingModel.php index 67e4c24..1d4d994 100644 --- a/sources/app/Model/UserLocking.php +++ b/sources/app/Model/UserLockingModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * User Locking Model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class UserLocking extends Base +class UserLockingModel extends Base { /** * Get the number of failed login for the user @@ -19,7 +21,7 @@ class UserLocking extends Base */ public function getFailedLogin($username) { - return (int) $this->db->table(User::TABLE) + return (int) $this->db->table(UserModel::TABLE) ->eq('username', $username) ->findOneColumn('nb_failed_login'); } @@ -33,7 +35,7 @@ class UserLocking extends Base */ public function resetFailedLogin($username) { - return $this->db->table(User::TABLE) + return $this->db->table(UserModel::TABLE) ->eq('username', $username) ->update(array( 'nb_failed_login' => 0, @@ -50,7 +52,7 @@ class UserLocking extends Base */ public function incrementFailedLogin($username) { - return $this->db->table(User::TABLE) + return $this->db->table(UserModel::TABLE) ->eq('username', $username) ->increment('nb_failed_login', 1); } @@ -64,7 +66,7 @@ class UserLocking extends Base */ public function isLocked($username) { - return $this->db->table(User::TABLE) + return $this->db->table(UserModel::TABLE) ->eq('username', $username) ->neq('lock_expiration_date', 0) ->gte('lock_expiration_date', time()) @@ -81,7 +83,7 @@ class UserLocking extends Base */ public function lock($username, $duration = 15) { - return $this->db->table(User::TABLE) + return $this->db->table(UserModel::TABLE) ->eq('username', $username) ->update(array( 'lock_expiration_date' => time() + $duration * 60 diff --git a/sources/app/Model/UserMention.php b/sources/app/Model/UserMentionModel.php similarity index 79% rename from sources/app/Model/UserMention.php rename to sources/app/Model/UserMentionModel.php index 97a4e41..cdb9949 100644 --- a/sources/app/Model/UserMention.php +++ b/sources/app/Model/UserMentionModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Event\GenericEvent; /** * User Mention * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class UserMention extends Base +class UserMentionModel extends Base { /** * Get list of mentioned users @@ -24,7 +25,7 @@ class UserMention extends Base $users = array(); if (preg_match_all('/@([^\s]+)/', $content, $matches)) { - $users = $this->db->table(User::TABLE) + $users = $this->db->table(UserModel::TABLE) ->columns('id', 'username', 'name', 'email', 'language') ->eq('notifications_enabled', 1) ->neq('id', $this->userSession->getId()) @@ -46,13 +47,13 @@ class UserMention extends Base public function fireEvents($content, $eventName, GenericEvent $event) { if (empty($event['project_id'])) { - $event['project_id'] = $this->taskFinder->getProjectId($event['task_id']); + $event['project_id'] = $this->taskFinderModel->getProjectId($event['task_id']); } $users = $this->getMentionedUsers($content); foreach ($users as $user) { - if ($this->projectPermission->isMember($event['project_id'], $user['id'])) { + if ($this->projectPermissionModel->isMember($event['project_id'], $user['id'])) { $event['mention'] = $user; $this->dispatcher->dispatch($eventName, $event); } diff --git a/sources/app/Model/UserMetadata.php b/sources/app/Model/UserMetadataModel.php similarity index 55% rename from sources/app/Model/UserMetadata.php rename to sources/app/Model/UserMetadataModel.php index 411837b..e931d3b 100644 --- a/sources/app/Model/UserMetadata.php +++ b/sources/app/Model/UserMetadataModel.php @@ -5,17 +5,22 @@ namespace Kanboard\Model; /** * User Metadata * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class UserMetadata extends Metadata +class UserMetadataModel extends MetadataModel { /** - * SQL table name + * Get the table * - * @var string + * @abstract + * @access protected + * @return string */ - const TABLE = 'user_has_metadata'; + protected function getTable() + { + return 'user_has_metadata'; + } /** * Define the entity key diff --git a/sources/app/Model/User.php b/sources/app/Model/UserModel.php similarity index 88% rename from sources/app/Model/User.php rename to sources/app/Model/UserModel.php index 5799300..f7a051c 100644 --- a/sources/app/Model/User.php +++ b/sources/app/Model/UserModel.php @@ -3,16 +3,17 @@ namespace Kanboard\Model; use PicoDb\Database; +use Kanboard\Core\Base; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; /** * User model * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class User extends Base +class UserModel extends Base { /** * SQL table name @@ -22,7 +23,7 @@ class User extends Base const TABLE = 'users'; /** - * Id used for everbody (filtering) + * Id used for everybody (filtering) * * @var integer */ @@ -85,7 +86,7 @@ class User extends Base { return $this->userSession->isAdmin() || // Avoid SQL query if connected $this->db - ->table(User::TABLE) + ->table(UserModel::TABLE) ->eq('id', $user_id) ->eq('role', Role::APP_ADMIN) ->exists(); @@ -211,7 +212,7 @@ class User extends Base $listing = $this->prepareList($users); if ($prepend) { - return array(User::EVERYBODY_ID => t('Everybody')) + $listing; + return array(UserModel::EVERYBODY_ID => t('Everybody')) + $listing; } return $listing; @@ -269,7 +270,7 @@ class User extends Base public function create(array $values) { $this->prepare($values); - return $this->persist(self::TABLE, $values); + return $this->db->table(self::TABLE)->persist($values); } /** @@ -320,38 +321,38 @@ class User extends Base */ public function remove($user_id) { - $this->avatarFile->remove($user_id); + $this->avatarFileModel->remove($user_id); return $this->db->transaction(function (Database $db) use ($user_id) { // All assigned tasks are now unassigned (no foreign key) - if (! $db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) { + if (! $db->table(TaskModel::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) { return false; } // All assigned subtasks are now unassigned (no foreign key) - if (! $db->table(Subtask::TABLE)->eq('user_id', $user_id)->update(array('user_id' => 0))) { + if (! $db->table(SubtaskModel::TABLE)->eq('user_id', $user_id)->update(array('user_id' => 0))) { return false; } // All comments are not assigned anymore (no foreign key) - if (! $db->table(Comment::TABLE)->eq('user_id', $user_id)->update(array('user_id' => 0))) { + if (! $db->table(CommentModel::TABLE)->eq('user_id', $user_id)->update(array('user_id' => 0))) { return false; } // All private projects are removed - $project_ids = $db->table(Project::TABLE) + $project_ids = $db->table(ProjectModel::TABLE) ->eq('is_private', 1) - ->eq(ProjectUserRole::TABLE.'.user_id', $user_id) - ->join(ProjectUserRole::TABLE, 'project_id', 'id') - ->findAllByColumn(Project::TABLE.'.id'); + ->eq(ProjectUserRoleModel::TABLE.'.user_id', $user_id) + ->join(ProjectUserRoleModel::TABLE, 'project_id', 'id') + ->findAllByColumn(ProjectModel::TABLE.'.id'); if (! empty($project_ids)) { - $db->table(Project::TABLE)->in('id', $project_ids)->remove(); + $db->table(ProjectModel::TABLE)->in('id', $project_ids)->remove(); } // Finally remove the user - if (! $db->table(User::TABLE)->eq('id', $user_id)->remove()) { + if (! $db->table(UserModel::TABLE)->eq('id', $user_id)->remove()) { return false; } }); diff --git a/sources/app/Model/UserNotificationFilter.php b/sources/app/Model/UserNotificationFilterModel.php similarity index 94% rename from sources/app/Model/UserNotificationFilter.php rename to sources/app/Model/UserNotificationFilterModel.php index 780ddfc..112ba29 100644 --- a/sources/app/Model/UserNotificationFilter.php +++ b/sources/app/Model/UserNotificationFilterModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * User Notification Filter * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class UserNotificationFilter extends Base +class UserNotificationFilterModel extends Base { /** * SQL table name @@ -52,7 +54,7 @@ class UserNotificationFilter extends Base */ public function getSelectedFilter($user_id) { - return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter'); + return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter'); } /** @@ -65,7 +67,7 @@ class UserNotificationFilter extends Base */ public function saveFilter($user_id, $filter) { - return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array( 'notifications_filter' => $filter, )); } diff --git a/sources/app/Model/UserNotification.php b/sources/app/Model/UserNotificationModel.php similarity index 60% rename from sources/app/Model/UserNotification.php rename to sources/app/Model/UserNotificationModel.php index 7795da2..d77526f 100644 --- a/sources/app/Model/UserNotification.php +++ b/sources/app/Model/UserNotificationModel.php @@ -2,15 +2,16 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; use Kanboard\Core\Translator; /** * User Notification * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class UserNotification extends Base +class UserNotificationModel extends Base { /** * Send notifications to people @@ -24,7 +25,7 @@ class UserNotification extends Base $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $this->userSession->getId()); foreach ($users as $user) { - if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) { + if ($this->userNotificationFilterModel->shouldReceiveNotification($user, $event_data)) { $this->sendUserNotification($user, $event_name, $event_data); } } @@ -46,15 +47,15 @@ class UserNotification extends Base if (! empty($user['language'])) { Translator::load($user['language']); } else { - Translator::load($this->config->get('application_language', 'en_US')); + Translator::load($this->configModel->get('application_language', 'en_US')); } - foreach ($this->userNotificationType->getSelectedTypes($user['id']) as $type) { - $this->userNotificationType->getType($type)->notifyUser($user, $event_name, $event_data); + foreach ($this->userNotificationTypeModel->getSelectedTypes($user['id']) as $type) { + $this->userNotificationTypeModel->getType($type)->notifyUser($user, $event_name, $event_data); } // Restore locales - $this->config->setupTranslations(); + $this->languageModel->loadCurrentLanguage(); } /** @@ -67,7 +68,7 @@ class UserNotification extends Base */ public function getUsersWithNotificationEnabled($project_id, $exclude_user_id = 0) { - if ($this->projectPermission->isEverybodyAllowed($project_id)) { + if ($this->projectPermissionModel->isEverybodyAllowed($project_id)) { return $this->getEverybodyWithNotificationEnabled($exclude_user_id); } @@ -93,7 +94,7 @@ class UserNotification extends Base */ public function enableNotification($user_id) { - return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 1)); + return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 1)); } /** @@ -105,7 +106,7 @@ class UserNotification extends Base */ public function disableNotification($user_id) { - return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 0)); + return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 0)); } /** @@ -125,12 +126,12 @@ class UserNotification extends Base $this->disableNotification($user_id); } - $filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter']; + $filter = empty($values['notifications_filter']) ? UserNotificationFilterModel::FILTER_BOTH : $values['notifications_filter']; $project_ids = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']); - $this->userNotificationFilter->saveFilter($user_id, $filter); - $this->userNotificationFilter->saveSelectedProjects($user_id, $project_ids); - $this->userNotificationType->saveSelectedTypes($user_id, $types); + $this->userNotificationFilterModel->saveFilter($user_id, $filter); + $this->userNotificationFilterModel->saveSelectedProjects($user_id, $project_ids); + $this->userNotificationTypeModel->saveSelectedTypes($user_id, $types); } /** @@ -142,9 +143,9 @@ class UserNotification extends Base */ public function readSettings($user_id) { - $values = $this->db->table(User::TABLE)->eq('id', $user_id)->columns('notifications_enabled', 'notifications_filter')->findOne(); - $values['notification_types'] = $this->userNotificationType->getSelectedTypes($user_id); - $values['notification_projects'] = $this->userNotificationFilter->getSelectedProjects($user_id); + $values = $this->db->table(UserModel::TABLE)->eq('id', $user_id)->columns('notifications_enabled', 'notifications_filter')->findOne(); + $values['notification_types'] = $this->userNotificationTypeModel->getSelectedTypes($user_id); + $values['notification_projects'] = $this->userNotificationFilterModel->getSelectedProjects($user_id); return $values; } @@ -159,25 +160,27 @@ class UserNotification extends Base private function getProjectUserMembersWithNotificationEnabled($project_id, $exclude_user_id) { return $this->db - ->table(ProjectUserRole::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') - ->join(User::TABLE, 'id', 'user_id') - ->eq('project_id', $project_id) - ->eq('notifications_enabled', '1') - ->neq(User::TABLE.'.id', $exclude_user_id) + ->table(ProjectUserRoleModel::TABLE) + ->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name', UserModel::TABLE.'.email', UserModel::TABLE.'.language', UserModel::TABLE.'.notifications_filter') + ->join(UserModel::TABLE, 'id', 'user_id') + ->eq(ProjectUserRoleModel::TABLE.'.project_id', $project_id) + ->eq(UserModel::TABLE.'.notifications_enabled', '1') + ->eq(UserModel::TABLE.'.is_active', 1) + ->neq(UserModel::TABLE.'.id', $exclude_user_id) ->findAll(); } private function getProjectGroupMembersWithNotificationEnabled($project_id, $exclude_user_id) { return $this->db - ->table(ProjectGroupRole::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') - ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE) - ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE) - ->eq(ProjectGroupRole::TABLE.'.project_id', $project_id) - ->eq(User::TABLE.'.notifications_enabled', '1') - ->neq(User::TABLE.'.id', $exclude_user_id) + ->table(ProjectGroupRoleModel::TABLE) + ->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name', UserModel::TABLE.'.email', UserModel::TABLE.'.language', UserModel::TABLE.'.notifications_filter') + ->join(GroupMemberModel::TABLE, 'group_id', 'group_id', ProjectGroupRoleModel::TABLE) + ->join(UserModel::TABLE, 'id', 'user_id', GroupMemberModel::TABLE) + ->eq(ProjectGroupRoleModel::TABLE.'.project_id', $project_id) + ->eq(UserModel::TABLE.'.notifications_enabled', '1') + ->neq(UserModel::TABLE.'.id', $exclude_user_id) + ->eq(UserModel::TABLE.'.is_active', 1) ->findAll(); } @@ -191,10 +194,11 @@ class UserNotification extends Base private function getEverybodyWithNotificationEnabled($exclude_user_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') + ->table(UserModel::TABLE) + ->columns(UserModel::TABLE.'.id', UserModel::TABLE.'.username', UserModel::TABLE.'.name', UserModel::TABLE.'.email', UserModel::TABLE.'.language', UserModel::TABLE.'.notifications_filter') ->eq('notifications_enabled', '1') - ->neq(User::TABLE.'.id', $exclude_user_id) + ->neq(UserModel::TABLE.'.id', $exclude_user_id) + ->eq(UserModel::TABLE.'.is_active', 1) ->findAll(); } } diff --git a/sources/app/Model/UserNotificationType.php b/sources/app/Model/UserNotificationTypeModel.php similarity index 92% rename from sources/app/Model/UserNotificationType.php rename to sources/app/Model/UserNotificationTypeModel.php index 89beb48..0f37722 100644 --- a/sources/app/Model/UserNotificationType.php +++ b/sources/app/Model/UserNotificationTypeModel.php @@ -5,10 +5,10 @@ namespace Kanboard\Model; /** * User Notification Type * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class UserNotificationType extends NotificationType +class UserNotificationTypeModel extends NotificationTypeModel { /** * SQL table name diff --git a/sources/app/Model/UserUnreadNotification.php b/sources/app/Model/UserUnreadNotificationModel.php similarity index 72% rename from sources/app/Model/UserUnreadNotification.php rename to sources/app/Model/UserUnreadNotificationModel.php index cc0f326..6c930ee 100644 --- a/sources/app/Model/UserUnreadNotification.php +++ b/sources/app/Model/UserUnreadNotificationModel.php @@ -2,13 +2,15 @@ namespace Kanboard\Model; +use Kanboard\Core\Base; + /** * User Unread Notification * - * @package model + * @package Kanboard\Model * @author Frederic Guillot */ -class UserUnreadNotification extends Base +class UserUnreadNotificationModel extends Base { /** * SQL table name @@ -35,6 +37,23 @@ class UserUnreadNotification extends Base )); } + /** + * Get one notification + * + * @param integer $notification_id + * @return array|null + */ + public function getById($notification_id) + { + $notification = $this->db->table(self::TABLE)->eq('id', $notification_id)->findOne(); + + if (! empty($notification)) { + $this->unserialize($notification); + } + + return $notification; + } + /** * Get all notifications for a user * @@ -47,8 +66,7 @@ class UserUnreadNotification extends Base $events = $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('date_creation')->findAll(); foreach ($events as &$event) { - $event['event_data'] = json_decode($event['event_data'], true); - $event['title'] = $this->notification->getTitleWithoutAuthor($event['event_name'], $event['event_data']); + $this->unserialize($event); } return $events; @@ -90,4 +108,10 @@ class UserUnreadNotification extends Base { return $this->db->table(self::TABLE)->eq('user_id', $user_id)->exists(); } + + private function unserialize(&$event) + { + $event['event_data'] = json_decode($event['event_data'], true); + $event['title'] = $this->notificationModel->getTitleWithoutAuthor($event['event_name'], $event['event_data']); + } } diff --git a/sources/app/Notification/ActivityStream.php b/sources/app/Notification/ActivityStreamNotification.php similarity index 81% rename from sources/app/Notification/ActivityStream.php rename to sources/app/Notification/ActivityStreamNotification.php index 325732e..9f23c88 100644 --- a/sources/app/Notification/ActivityStream.php +++ b/sources/app/Notification/ActivityStreamNotification.php @@ -3,14 +3,15 @@ namespace Kanboard\Notification; use Kanboard\Core\Base; +use Kanboard\Core\Notification\NotificationInterface; /** * Activity Stream Notification * - * @package notification + * @package Kanboard\Notification * @author Frederic Guillot */ -class ActivityStream extends Base implements NotificationInterface +class ActivityStreamNotification extends Base implements NotificationInterface { /** * Send notification to a user @@ -35,7 +36,7 @@ class ActivityStream extends Base implements NotificationInterface public function notifyProject(array $project, $event_name, array $event_data) { if ($this->userSession->isLogged()) { - $this->projectActivity->createEvent( + $this->projectActivityModel->createEvent( $project['id'], $event_data['task']['id'], $this->userSession->getId(), diff --git a/sources/app/Notification/Mail.php b/sources/app/Notification/MailNotification.php similarity index 79% rename from sources/app/Notification/Mail.php rename to sources/app/Notification/MailNotification.php index c924fb5..2d27179 100644 --- a/sources/app/Notification/Mail.php +++ b/sources/app/Notification/MailNotification.php @@ -3,18 +3,19 @@ namespace Kanboard\Notification; use Kanboard\Core\Base; -use Kanboard\Model\Task; -use Kanboard\Model\TaskFile; -use Kanboard\Model\Comment; -use Kanboard\Model\Subtask; +use Kanboard\Core\Notification\NotificationInterface; +use Kanboard\Model\TaskModel; +use Kanboard\Model\TaskFileModel; +use Kanboard\Model\CommentModel; +use Kanboard\Model\SubtaskModel; /** * Email Notification * - * @package notification + * @package Kanboard\Notification * @author Frederic Guillot */ -class Mail extends Base implements NotificationInterface +class MailNotification extends Base implements NotificationInterface { /** * Notification type @@ -67,7 +68,7 @@ class Mail extends Base implements NotificationInterface { return $this->template->render( 'notification/'.str_replace('.', '_', $event_name), - $event_data + array('application_url' => $this->config->get('application_url')) + $event_data + array('application_url' => $this->configModel->get('application_url')) ); } @@ -82,50 +83,50 @@ class Mail extends Base implements NotificationInterface public function getMailSubject($event_name, array $event_data) { switch ($event_name) { - case TaskFile::EVENT_CREATE: + case TaskFileModel::EVENT_CREATE: $subject = $this->getStandardMailSubject(e('New attachment'), $event_data); break; - case Comment::EVENT_CREATE: + case CommentModel::EVENT_CREATE: $subject = $this->getStandardMailSubject(e('New comment'), $event_data); break; - case Comment::EVENT_UPDATE: + case CommentModel::EVENT_UPDATE: $subject = $this->getStandardMailSubject(e('Comment updated'), $event_data); break; - case Subtask::EVENT_CREATE: + case SubtaskModel::EVENT_CREATE: $subject = $this->getStandardMailSubject(e('New subtask'), $event_data); break; - case Subtask::EVENT_UPDATE: + case SubtaskModel::EVENT_UPDATE: $subject = $this->getStandardMailSubject(e('Subtask updated'), $event_data); break; - case Task::EVENT_CREATE: + case TaskModel::EVENT_CREATE: $subject = $this->getStandardMailSubject(e('New task'), $event_data); break; - case Task::EVENT_UPDATE: + case TaskModel::EVENT_UPDATE: $subject = $this->getStandardMailSubject(e('Task updated'), $event_data); break; - case Task::EVENT_CLOSE: + case TaskModel::EVENT_CLOSE: $subject = $this->getStandardMailSubject(e('Task closed'), $event_data); break; - case Task::EVENT_OPEN: + case TaskModel::EVENT_OPEN: $subject = $this->getStandardMailSubject(e('Task opened'), $event_data); break; - case Task::EVENT_MOVE_COLUMN: + case TaskModel::EVENT_MOVE_COLUMN: $subject = $this->getStandardMailSubject(e('Column change'), $event_data); break; - case Task::EVENT_MOVE_POSITION: + case TaskModel::EVENT_MOVE_POSITION: $subject = $this->getStandardMailSubject(e('Position change'), $event_data); break; - case Task::EVENT_MOVE_SWIMLANE: + case TaskModel::EVENT_MOVE_SWIMLANE: $subject = $this->getStandardMailSubject(e('Swimlane change'), $event_data); break; - case Task::EVENT_ASSIGNEE_CHANGE: + case TaskModel::EVENT_ASSIGNEE_CHANGE: $subject = $this->getStandardMailSubject(e('Assignee change'), $event_data); break; - case Task::EVENT_USER_MENTION: - case Comment::EVENT_USER_MENTION: + case TaskModel::EVENT_USER_MENTION: + case CommentModel::EVENT_USER_MENTION: $subject = $this->getStandardMailSubject(e('Mentioned'), $event_data); break; - case Task::EVENT_OVERDUE: + case TaskModel::EVENT_OVERDUE: $subject = e('[%s] Overdue tasks', $event_data['project_name']); break; default: diff --git a/sources/app/Notification/Web.php b/sources/app/Notification/WebNotification.php similarity index 75% rename from sources/app/Notification/Web.php rename to sources/app/Notification/WebNotification.php index 9271c19..d881882 100644 --- a/sources/app/Notification/Web.php +++ b/sources/app/Notification/WebNotification.php @@ -3,14 +3,15 @@ namespace Kanboard\Notification; use Kanboard\Core\Base; +use Kanboard\Core\Notification\NotificationInterface; /** * Web Notification * - * @package notification + * @package Kanboard\Notification * @author Frederic Guillot */ -class Web extends Base implements NotificationInterface +class WebNotification extends Base implements NotificationInterface { /** * Notification type @@ -29,7 +30,7 @@ class Web extends Base implements NotificationInterface */ public function notifyUser(array $user, $event_name, array $event_data) { - $this->userUnreadNotification->create($user['id'], $event_name, $event_data); + $this->userUnreadNotificationModel->create($user['id'], $event_name, $event_data); } /** diff --git a/sources/app/Notification/Webhook.php b/sources/app/Notification/WebhookNotification.php similarity index 79% rename from sources/app/Notification/Webhook.php rename to sources/app/Notification/WebhookNotification.php index e187909..1604553 100644 --- a/sources/app/Notification/Webhook.php +++ b/sources/app/Notification/WebhookNotification.php @@ -3,14 +3,15 @@ namespace Kanboard\Notification; use Kanboard\Core\Base; +use Kanboard\Core\Notification\NotificationInterface; /** * Webhook Notification * - * @package notification + * @package Kanboard\Notification * @author Frederic Guillot */ -class Webhook extends Base implements NotificationInterface +class WebhookNotification extends Base implements NotificationInterface { /** * Send notification to a user @@ -34,8 +35,8 @@ class Webhook extends Base implements NotificationInterface */ public function notifyProject(array $project, $event_name, array $event_data) { - $url = $this->config->get('webhook_url'); - $token = $this->config->get('webhook_token'); + $url = $this->configModel->get('webhook_url'); + $token = $this->configModel->get('webhook_token'); if (! empty($url)) { if (strpos($url, '?') !== false) { diff --git a/sources/app/Schema/Mysql.php b/sources/app/Schema/Mysql.php index 934b063..82ccb8c 100644 --- a/sources/app/Schema/Mysql.php +++ b/sources/app/Schema/Mysql.php @@ -6,7 +6,30 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 110; +const VERSION = 111; + +function version_111(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE tags ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + project_id INT NOT NULL, + UNIQUE(project_id, name), + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec(" + CREATE TABLE task_has_tags ( + task_id INT NOT NULL, + tag_id INT NOT NULL, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE, + UNIQUE(tag_id, task_id) + ) ENGINE=InnoDB CHARSET=utf8 + "); +} function version_110(PDO $pdo) { diff --git a/sources/app/Schema/Postgres.php b/sources/app/Schema/Postgres.php index 3ef4949..229cbd2 100644 --- a/sources/app/Schema/Postgres.php +++ b/sources/app/Schema/Postgres.php @@ -6,7 +6,29 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 89; +const VERSION = 90; + +function version_90(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + project_id INTEGER NOT NULL, + UNIQUE(project_id, name) + ) + "); + + $pdo->exec(" + CREATE TABLE task_has_tags ( + task_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE, + UNIQUE(tag_id, task_id) + ) + "); +} function version_89(PDO $pdo) { diff --git a/sources/app/Schema/Sqlite.php b/sources/app/Schema/Sqlite.php index 9ded7ed..dac348d 100644 --- a/sources/app/Schema/Sqlite.php +++ b/sources/app/Schema/Sqlite.php @@ -6,7 +6,29 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 101; +const VERSION = 102; + +function version_102(PDO $pdo) +{ + $pdo->exec(" + CREATE TABLE tags ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + project_id INTEGER NOT NULL, + UNIQUE(project_id, name) + ) + "); + + $pdo->exec(" + CREATE TABLE task_has_tags ( + task_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE, + UNIQUE(tag_id, task_id) + ) + "); +} function version_101(PDO $pdo) { diff --git a/sources/app/ServiceProvider/ActionProvider.php b/sources/app/ServiceProvider/ActionProvider.php index 3692f19..3420205 100644 --- a/sources/app/ServiceProvider/ActionProvider.php +++ b/sources/app/ServiceProvider/ActionProvider.php @@ -2,6 +2,7 @@ namespace Kanboard\ServiceProvider; +use Kanboard\Action\TaskAssignColorPriority; use Pimple\Container; use Pimple\ServiceProviderInterface; use Kanboard\Core\Action\ActionManager; @@ -35,7 +36,7 @@ use Kanboard\Action\TaskCloseNoActivity; /** * Action Provider * - * @package serviceProvider + * @package Kanboard\ServiceProvider * @author Frederic Guillot */ class ActionProvider implements ServiceProviderInterface @@ -59,6 +60,7 @@ class ActionProvider implements ServiceProviderInterface $container['actionManager']->register(new TaskAssignColorColumn($container)); $container['actionManager']->register(new TaskAssignColorLink($container)); $container['actionManager']->register(new TaskAssignColorUser($container)); + $container['actionManager']->register(new TaskAssignColorPriority($container)); $container['actionManager']->register(new TaskAssignCurrentUser($container)); $container['actionManager']->register(new TaskAssignCurrentUserColumn($container)); $container['actionManager']->register(new TaskAssignSpecificUser($container)); diff --git a/sources/app/ServiceProvider/ApiProvider.php b/sources/app/ServiceProvider/ApiProvider.php new file mode 100644 index 0000000..5cf6231 --- /dev/null +++ b/sources/app/ServiceProvider/ApiProvider.php @@ -0,0 +1,81 @@ +setAuthenticationHeader(API_AUTHENTICATION_HEADER); + $server->getMiddlewareHandler() + ->withMiddleware(new AuthenticationMiddleware($container)) + ; + + $server->getProcedureHandler() + ->withObject(new MeProcedure($container)) + ->withObject(new ActionProcedure($container)) + ->withObject(new AppProcedure($container)) + ->withObject(new BoardProcedure($container)) + ->withObject(new ColumnProcedure($container)) + ->withObject(new CategoryProcedure($container)) + ->withObject(new CommentProcedure($container)) + ->withObject(new TaskFileProcedure($container)) + ->withObject(new ProjectFileProcedure($container)) + ->withObject(new LinkProcedure($container)) + ->withObject(new ProjectProcedure($container)) + ->withObject(new ProjectPermissionProcedure($container)) + ->withObject(new SubtaskProcedure($container)) + ->withObject(new SubtaskTimeTrackingProcedure($container)) + ->withObject(new SwimlaneProcedure($container)) + ->withObject(new TaskProcedure($container)) + ->withObject(new TaskLinkProcedure($container)) + ->withObject(new TaskExternalLinkProcedure($container)) + ->withObject(new UserProcedure($container)) + ->withObject(new GroupProcedure($container)) + ->withObject(new GroupMemberProcedure($container)) + ->withBeforeMethod('beforeProcedure') + ; + + $container['api'] = $server; + return $container; + } +} diff --git a/sources/app/ServiceProvider/AuthenticationProvider.php b/sources/app/ServiceProvider/AuthenticationProvider.php index 776e65d..978bc05 100644 --- a/sources/app/ServiceProvider/AuthenticationProvider.php +++ b/sources/app/ServiceProvider/AuthenticationProvider.php @@ -17,7 +17,7 @@ use Kanboard\Auth\ReverseProxyAuth; /** * Authentication Provider * - * @package serviceProvider + * @package Kanboard\ServiceProvider * @author Frederic Guillot */ class AuthenticationProvider implements ServiceProviderInterface @@ -46,9 +46,13 @@ class AuthenticationProvider implements ServiceProviderInterface $container['projectAccessMap'] = $this->getProjectAccessMap(); $container['applicationAccessMap'] = $this->getApplicationAccessMap(); + $container['apiAccessMap'] = $this->getApiAccessMap(); + $container['apiProjectAccessMap'] = $this->getApiProjectAccessMap(); $container['projectAuthorization'] = new Authorization($container['projectAccessMap']); $container['applicationAuthorization'] = new Authorization($container['applicationAccessMap']); + $container['apiAuthorization'] = new Authorization($container['apiAccessMap']); + $container['apiProjectAuthorization'] = new Authorization($container['apiProjectAccessMap']); return $container; } @@ -66,39 +70,44 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->setRoleHierarchy(Role::PROJECT_MANAGER, array(Role::PROJECT_MEMBER, Role::PROJECT_VIEWER)); $acl->setRoleHierarchy(Role::PROJECT_MEMBER, array(Role::PROJECT_VIEWER)); - $acl->add('Action', '*', Role::PROJECT_MANAGER); - $acl->add('ActionProject', '*', Role::PROJECT_MANAGER); - $acl->add('ActionCreation', '*', Role::PROJECT_MANAGER); - $acl->add('Analytic', '*', Role::PROJECT_MANAGER); - $acl->add('Board', 'save', Role::PROJECT_MEMBER); - $acl->add('BoardPopover', '*', Role::PROJECT_MEMBER); - $acl->add('Calendar', 'save', Role::PROJECT_MEMBER); - $acl->add('Category', '*', Role::PROJECT_MANAGER); - $acl->add('Column', '*', Role::PROJECT_MANAGER); - $acl->add('Comment', '*', Role::PROJECT_MEMBER); - $acl->add('Customfilter', '*', Role::PROJECT_MEMBER); - $acl->add('Export', '*', Role::PROJECT_MANAGER); - $acl->add('TaskFile', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER); - $acl->add('Gantt', '*', Role::PROJECT_MANAGER); - $acl->add('Project', array('share', 'integrations', 'notifications', 'duplicate', 'disable', 'enable', 'remove'), Role::PROJECT_MANAGER); - $acl->add('ProjectPermission', '*', Role::PROJECT_MANAGER); - $acl->add('ProjectEdit', '*', Role::PROJECT_MANAGER); - $acl->add('ProjectFile', '*', Role::PROJECT_MEMBER); - $acl->add('Projectuser', '*', Role::PROJECT_MANAGER); - $acl->add('Subtask', '*', Role::PROJECT_MEMBER); - $acl->add('SubtaskRestriction', '*', Role::PROJECT_MEMBER); - $acl->add('SubtaskStatus', '*', Role::PROJECT_MEMBER); - $acl->add('Swimlane', '*', Role::PROJECT_MANAGER); - $acl->add('Task', 'remove', Role::PROJECT_MEMBER); - $acl->add('Taskcreation', '*', Role::PROJECT_MEMBER); - $acl->add('Taskduplication', '*', Role::PROJECT_MEMBER); - $acl->add('TaskRecurrence', '*', Role::PROJECT_MEMBER); - $acl->add('TaskImport', '*', Role::PROJECT_MANAGER); - $acl->add('TaskInternalLink', '*', Role::PROJECT_MEMBER); - $acl->add('TaskExternalLink', '*', Role::PROJECT_MEMBER); - $acl->add('Taskmodification', '*', Role::PROJECT_MEMBER); - $acl->add('Taskstatus', '*', Role::PROJECT_MEMBER); - $acl->add('UserHelper', array('mention'), Role::PROJECT_MEMBER); + $acl->add('ActionController', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectActionDuplicationController', '*', Role::PROJECT_MANAGER); + $acl->add('ActionCreationController', '*', Role::PROJECT_MANAGER); + $acl->add('AnalyticController', '*', Role::PROJECT_MANAGER); + $acl->add('BoardAjaxController', 'save', Role::PROJECT_MEMBER); + $acl->add('BoardPopoverController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskPopoverController', '*', Role::PROJECT_MEMBER); + $acl->add('CalendarController', 'save', Role::PROJECT_MEMBER); + $acl->add('CategoryController', '*', Role::PROJECT_MANAGER); + $acl->add('ColumnController', '*', Role::PROJECT_MANAGER); + $acl->add('CommentController', '*', Role::PROJECT_MEMBER); + $acl->add('CustomFilterController', '*', Role::PROJECT_MEMBER); + $acl->add('ExportController', '*', Role::PROJECT_MANAGER); + $acl->add('TaskFileController', array('screenshot', 'create', 'save', 'remove', 'confirm'), Role::PROJECT_MEMBER); + $acl->add('TaskGanttController', '*', Role::PROJECT_MANAGER); + $acl->add('TaskGanttCreationController', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectViewController', array('share', 'updateSharing', 'integrations', 'updateIntegrations', 'notifications', 'updateNotifications', 'duplicate', 'doDuplication'), Role::PROJECT_MANAGER); + $acl->add('ProjectPermissionController', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectEditController', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectFileController', '*', Role::PROJECT_MEMBER); + $acl->add('ProjectUserOverviewController', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectStatusController', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectTagController', '*', Role::PROJECT_MANAGER); + $acl->add('SubtaskController', '*', Role::PROJECT_MEMBER); + $acl->add('SubtaskRestrictionController', '*', Role::PROJECT_MEMBER); + $acl->add('SubtaskStatusController', '*', Role::PROJECT_MEMBER); + $acl->add('SwimlaneController', '*', Role::PROJECT_MANAGER); + $acl->add('TaskSuppressionController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskCreationController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskBulkController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskDuplicationController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskRecurrenceController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskImportController', '*', Role::PROJECT_MANAGER); + $acl->add('TaskInternalLinkController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskExternalLinkController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskModificationController', '*', Role::PROJECT_MEMBER); + $acl->add('TaskStatusController', '*', Role::PROJECT_MEMBER); + $acl->add('UserAjaxController', array('mention'), Role::PROJECT_MEMBER); return $acl; } @@ -117,27 +126,87 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->setRoleHierarchy(Role::APP_MANAGER, array(Role::APP_USER, Role::APP_PUBLIC)); $acl->setRoleHierarchy(Role::APP_USER, array(Role::APP_PUBLIC)); - $acl->add('Auth', array('login', 'check'), Role::APP_PUBLIC); - $acl->add('Captcha', '*', Role::APP_PUBLIC); - $acl->add('PasswordReset', '*', Role::APP_PUBLIC); - $acl->add('Webhook', '*', Role::APP_PUBLIC); - $acl->add('Task', 'readonly', Role::APP_PUBLIC); - $acl->add('Board', 'readonly', Role::APP_PUBLIC); - $acl->add('Ical', '*', Role::APP_PUBLIC); - $acl->add('Feed', '*', Role::APP_PUBLIC); - $acl->add('AvatarFile', 'show', Role::APP_PUBLIC); + $acl->add('AuthController', array('login', 'check'), Role::APP_PUBLIC); + $acl->add('CaptchaController', '*', Role::APP_PUBLIC); + $acl->add('PasswordResetController', '*', Role::APP_PUBLIC); + $acl->add('TaskViewController', 'readonly', Role::APP_PUBLIC); + $acl->add('BoardViewController', 'readonly', Role::APP_PUBLIC); + $acl->add('ICalendarController', '*', Role::APP_PUBLIC); + $acl->add('FeedController', '*', Role::APP_PUBLIC); + $acl->add('AvatarFileController', 'show', Role::APP_PUBLIC); - $acl->add('Config', '*', Role::APP_ADMIN); - $acl->add('Currency', '*', Role::APP_ADMIN); - $acl->add('Gantt', array('projects', 'saveProjectDate'), Role::APP_MANAGER); - $acl->add('Group', '*', Role::APP_ADMIN); - $acl->add('Link', '*', Role::APP_ADMIN); - $acl->add('ProjectCreation', 'create', Role::APP_MANAGER); - $acl->add('Projectuser', '*', Role::APP_MANAGER); - $acl->add('Twofactor', 'disable', Role::APP_ADMIN); - $acl->add('UserImport', '*', Role::APP_ADMIN); - $acl->add('User', array('index', 'create', 'save', 'authentication'), Role::APP_ADMIN); - $acl->add('UserStatus', '*', Role::APP_ADMIN); + $acl->add('ConfigController', '*', Role::APP_ADMIN); + $acl->add('TagController', '*', Role::APP_ADMIN); + $acl->add('PluginController', '*', Role::APP_ADMIN); + $acl->add('CurrencyController', '*', Role::APP_ADMIN); + $acl->add('ProjectGanttController', '*', Role::APP_MANAGER); + $acl->add('GroupListController', '*', Role::APP_ADMIN); + $acl->add('GroupCreationController', '*', Role::APP_ADMIN); + $acl->add('GroupModificationController', '*', Role::APP_ADMIN); + $acl->add('LinkController', '*', Role::APP_ADMIN); + $acl->add('ProjectCreationController', 'create', Role::APP_MANAGER); + $acl->add('ProjectUserOverviewController', '*', Role::APP_MANAGER); + $acl->add('TwoFactorController', 'disable', Role::APP_ADMIN); + $acl->add('UserImportController', '*', Role::APP_ADMIN); + $acl->add('UserCreationController', '*', Role::APP_ADMIN); + $acl->add('UserListController', '*', Role::APP_ADMIN); + $acl->add('UserStatusController', '*', Role::APP_ADMIN); + $acl->add('UserCredentialController', array('changeAuthentication', 'saveAuthentication'), Role::APP_ADMIN); + + return $acl; + } + + /** + * Get ACL for the API + * + * @access public + * @return AccessMap + */ + public function getApiAccessMap() + { + $acl = new AccessMap; + $acl->setDefaultRole(Role::APP_USER); + $acl->setRoleHierarchy(Role::APP_ADMIN, array(Role::APP_MANAGER, Role::APP_USER, Role::APP_PUBLIC)); + $acl->setRoleHierarchy(Role::APP_MANAGER, array(Role::APP_USER, Role::APP_PUBLIC)); + + $acl->add('UserProcedure', '*', Role::APP_ADMIN); + $acl->add('GroupMemberProcedure', '*', Role::APP_ADMIN); + $acl->add('GroupProcedure', '*', Role::APP_ADMIN); + $acl->add('LinkProcedure', '*', Role::APP_ADMIN); + $acl->add('TaskProcedure', array('getOverdueTasks'), Role::APP_ADMIN); + $acl->add('ProjectProcedure', array('getAllProjects'), Role::APP_ADMIN); + $acl->add('ProjectProcedure', array('createProject'), Role::APP_MANAGER); + + return $acl; + } + + /** + * Get ACL for the API + * + * @access public + * @return AccessMap + */ + public function getApiProjectAccessMap() + { + $acl = new AccessMap; + $acl->setDefaultRole(Role::PROJECT_VIEWER); + $acl->setRoleHierarchy(Role::PROJECT_MANAGER, array(Role::PROJECT_MEMBER, Role::PROJECT_VIEWER)); + $acl->setRoleHierarchy(Role::PROJECT_MEMBER, array(Role::PROJECT_VIEWER)); + + $acl->add('ActionProcedure', array('removeAction', 'getActions', 'createAction'), Role::PROJECT_MANAGER); + $acl->add('CategoryProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('ColumnProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('CommentProcedure', array('removeComment', 'createComment', 'updateComment'), Role::PROJECT_MEMBER); + $acl->add('ProjectPermissionProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectProcedure', array('updateProject', 'removeProject', 'enableProject', 'disableProject', 'enableProjectPublicAccess', 'disableProjectPublicAccess'), Role::PROJECT_MANAGER); + $acl->add('SubtaskProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('SubtaskTimeTrackingProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('SwimlaneProcedure', '*', Role::PROJECT_MANAGER); + $acl->add('ProjectFileProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('TaskFileProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('TaskLinkProcedure', '*', Role::PROJECT_MEMBER); + $acl->add('TaskExternalLinkProcedure', array('createExternalTaskLink', 'updateExternalTaskLink', 'removeExternalTaskLink'), Role::PROJECT_MEMBER); + $acl->add('TaskProcedure', '*', Role::PROJECT_MEMBER); return $acl; } diff --git a/sources/app/ServiceProvider/AvatarProvider.php b/sources/app/ServiceProvider/AvatarProvider.php index aac4fca..d17985e 100644 --- a/sources/app/ServiceProvider/AvatarProvider.php +++ b/sources/app/ServiceProvider/AvatarProvider.php @@ -12,7 +12,7 @@ use Kanboard\User\Avatar\LetterAvatarProvider; /** * Avatar Provider * - * @package serviceProvider + * @package Kanboard\ServiceProvider * @author Frederic Guillot */ class AvatarProvider implements ServiceProviderInterface diff --git a/sources/app/ServiceProvider/ClassProvider.php b/sources/app/ServiceProvider/ClassProvider.php index 3e654a4..e32c0d4 100644 --- a/sources/app/ServiceProvider/ClassProvider.php +++ b/sources/app/ServiceProvider/ClassProvider.php @@ -4,13 +4,18 @@ namespace Kanboard\ServiceProvider; use Pimple\Container; use Pimple\ServiceProviderInterface; -use Kanboard\Core\Mail\Client as EmailClient; use Kanboard\Core\ObjectStorage\FileStorage; use Kanboard\Core\Paginator; use Kanboard\Core\Http\OAuth2; use Kanboard\Core\Tool; use Kanboard\Core\Http\Client as HttpClient; +/** + * Class ClassProvider + * + * @package Kanboard\ServiceProvider + * @author Frederic Guillot + */ class ClassProvider implements ServiceProviderInterface { private $classes = array( @@ -22,71 +27,68 @@ class ClassProvider implements ServiceProviderInterface 'AverageTimeSpentColumnAnalytic', ), 'Model' => array( - 'Action', - 'ActionParameter', - 'AvatarFile', - 'Board', - 'Category', - 'Color', - 'Column', - 'Comment', - 'Config', - 'Currency', - 'CustomFilter', - 'Group', - 'GroupMember', - 'LastLogin', - 'Link', - 'Notification', - 'PasswordReset', - 'Project', - 'ProjectFile', - 'ProjectActivity', - 'ProjectDuplication', - 'ProjectDailyColumnStats', - 'ProjectDailyStats', - 'ProjectPermission', - 'ProjectNotification', - 'ProjectMetadata', - 'ProjectGroupRole', - 'ProjectGroupRoleFilter', - 'ProjectUserRole', - 'ProjectUserRoleFilter', - 'RememberMeSession', - 'Subtask', - 'SubtaskTimeTracking', - 'Swimlane', - 'Task', - 'TaskAnalytic', - 'TaskCreation', - 'TaskDuplication', - 'TaskExternalLink', - 'TaskFinder', - 'TaskFile', - 'TaskFilter', - 'TaskLink', - 'TaskModification', - 'TaskPermission', - 'TaskPosition', - 'TaskStatus', - 'TaskMetadata', - 'Transition', - 'User', - 'UserLocking', - 'UserMention', - 'UserNotification', - 'UserNotificationFilter', - 'UserUnreadNotification', - 'UserMetadata', - ), - 'Formatter' => array( - 'TaskFilterGanttFormatter', - 'TaskFilterAutoCompleteFormatter', - 'TaskFilterCalendarFormatter', - 'TaskFilterICalendarFormatter', - 'ProjectGanttFormatter', - 'UserFilterAutoCompleteFormatter', - 'GroupAutoCompleteFormatter', + 'ActionModel', + 'ActionParameterModel', + 'AvatarFileModel', + 'BoardModel', + 'CategoryModel', + 'ColorModel', + 'ColumnModel', + 'CommentModel', + 'ConfigModel', + 'CurrencyModel', + 'CustomFilterModel', + 'GroupModel', + 'GroupMemberModel', + 'LanguageModel', + 'LastLoginModel', + 'LinkModel', + 'NotificationModel', + 'PasswordResetModel', + 'ProjectModel', + 'ProjectFileModel', + 'ProjectActivityModel', + 'ProjectDuplicationModel', + 'ProjectDailyColumnStatsModel', + 'ProjectDailyStatsModel', + 'ProjectPermissionModel', + 'ProjectNotificationModel', + 'ProjectMetadataModel', + 'ProjectGroupRoleModel', + 'ProjectTaskDuplicationModel', + 'ProjectTaskPriorityModel', + 'ProjectUserRoleModel', + 'RememberMeSessionModel', + 'SubtaskModel', + 'SubtaskTimeTrackingModel', + 'SwimlaneModel', + 'TagDuplicationModel', + 'TagModel', + 'TaskModel', + 'TaskAnalyticModel', + 'TaskCreationModel', + 'TaskDuplicationModel', + 'TaskProjectDuplicationModel', + 'TaskProjectMoveModel', + 'TaskRecurrenceModel', + 'TaskExternalLinkModel', + 'TaskFinderModel', + 'TaskFileModel', + 'TaskLinkModel', + 'TaskModificationModel', + 'TaskPositionModel', + 'TaskStatusModel', + 'TaskTagModel', + 'TaskMetadataModel', + 'TimezoneModel', + 'TransitionModel', + 'UserModel', + 'UserLockingModel', + 'UserMentionModel', + 'UserNotificationModel', + 'UserNotificationFilterModel', + 'UserUnreadNotificationModel', + 'UserMetadataModel', ), 'Validator' => array( 'ActionValidator', @@ -103,8 +105,9 @@ class ClassProvider implements ServiceProviderInterface 'ProjectValidator', 'SubtaskValidator', 'SwimlaneValidator', - 'TaskValidator', + 'TagValidator', 'TaskLinkValidator', + 'TaskValidator', 'UserValidator', ), 'Import' => array( @@ -166,14 +169,6 @@ class ClassProvider implements ServiceProviderInterface return new FileStorage(FILES_DIR); }; - $container['emailClient'] = function ($container) { - $mailer = new EmailClient($container); - $mailer->setTransport('smtp', '\Kanboard\Core\Mail\Transport\Smtp'); - $mailer->setTransport('sendmail', '\Kanboard\Core\Mail\Transport\Sendmail'); - $mailer->setTransport('mail', '\Kanboard\Core\Mail\Transport\Mail'); - return $mailer; - }; - $container['cspRules'] = array( 'default-src' => "'self'", 'style-src' => "'self' 'unsafe-inline'", diff --git a/sources/app/ServiceProvider/CommandProvider.php b/sources/app/ServiceProvider/CommandProvider.php new file mode 100644 index 0000000..55c2712 --- /dev/null +++ b/sources/app/ServiceProvider/CommandProvider.php @@ -0,0 +1,62 @@ +add(new TaskOverdueNotificationCommand($container)); + $application->add(new SubtaskExportCommand($container)); + $application->add(new TaskExportCommand($container)); + $application->add(new ProjectDailyStatsCalculationCommand($container)); + $application->add(new ProjectDailyColumnStatsExportCommand($container)); + $application->add(new TransitionExportCommand($container)); + $application->add(new LocaleSyncCommand($container)); + $application->add(new LocaleComparatorCommand($container)); + $application->add(new TaskTriggerCommand($container)); + $application->add(new CronjobCommand($container)); + $application->add(new WorkerCommand($container)); + $application->add(new ResetPasswordCommand($container)); + $application->add(new ResetTwoFactorCommand($container)); + $application->add(new PluginUpgradeCommand($container)); + $application->add(new PluginInstallCommand($container)); + $application->add(new PluginUninstallCommand($container)); + + $container['cli'] = $application; + return $container; + } +} diff --git a/sources/app/ServiceProvider/DatabaseProvider.php b/sources/app/ServiceProvider/DatabaseProvider.php index d323807..a3f5745 100644 --- a/sources/app/ServiceProvider/DatabaseProvider.php +++ b/sources/app/ServiceProvider/DatabaseProvider.php @@ -8,13 +8,31 @@ use Pimple\Container; use Pimple\ServiceProviderInterface; use PicoDb\Database; +/** + * Class DatabaseProvider + * + * @package Kanboard\ServiceProvider + * @author Frederic Guillot + */ class DatabaseProvider implements ServiceProviderInterface { + /** + * Register provider + * + * @access public + * @param Container $container + * @return Container + */ public function register(Container $container) { $container['db'] = $this->getInstance(); - $container['db']->stopwatch = DEBUG; - $container['db']->logQueries = DEBUG; + + if (DEBUG) { + $container['db']->getStatementHandler() + ->withLogging() + ->withStopWatch() + ; + } return $container; } @@ -83,6 +101,9 @@ class DatabaseProvider implements ServiceProviderInterface 'database' => DB_NAME, 'charset' => 'utf8', 'port' => DB_PORT, + 'ssl_key' => DB_SSL_KEY, + 'ssl_ca' => DB_SSL_CA, + 'ssl_cert' => DB_SSL_CERT, )); } diff --git a/sources/app/ServiceProvider/EventDispatcherProvider.php b/sources/app/ServiceProvider/EventDispatcherProvider.php index 880caa4..57543fe 100644 --- a/sources/app/ServiceProvider/EventDispatcherProvider.php +++ b/sources/app/ServiceProvider/EventDispatcherProvider.php @@ -2,6 +2,7 @@ namespace Kanboard\ServiceProvider; +use Kanboard\Subscriber\LdapUserPhotoSubscriber; use Pimple\Container; use Pimple\ServiceProviderInterface; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -14,6 +15,12 @@ use Kanboard\Subscriber\SubtaskTimeTrackingSubscriber; use Kanboard\Subscriber\TransitionSubscriber; use Kanboard\Subscriber\RecurringTaskSubscriber; +/** + * Class EventDispatcherProvider + * + * @package Kanboard\ServiceProvider + * @author Frederic Guillot + */ class EventDispatcherProvider implements ServiceProviderInterface { public function register(Container $container) @@ -28,6 +35,10 @@ class EventDispatcherProvider implements ServiceProviderInterface $container['dispatcher']->addSubscriber(new TransitionSubscriber($container)); $container['dispatcher']->addSubscriber(new RecurringTaskSubscriber($container)); + if (LDAP_AUTH && LDAP_USER_ATTRIBUTE_PHOTO !== '') { + $container['dispatcher']->addSubscriber(new LdapUserPhotoSubscriber($container)); + } + return $container; } } diff --git a/sources/app/ServiceProvider/ExternalLinkProvider.php b/sources/app/ServiceProvider/ExternalLinkProvider.php index 8b71ec8..2cec768 100644 --- a/sources/app/ServiceProvider/ExternalLinkProvider.php +++ b/sources/app/ServiceProvider/ExternalLinkProvider.php @@ -12,7 +12,7 @@ use Kanboard\ExternalLink\FileLinkProvider; /** * External Link Provider * - * @package serviceProvider + * @package Kanboard\ServiceProvider * @author Frederic Guillot */ class ExternalLinkProvider implements ServiceProviderInterface diff --git a/sources/app/ServiceProvider/FilterProvider.php b/sources/app/ServiceProvider/FilterProvider.php new file mode 100644 index 0000000..20281a0 --- /dev/null +++ b/sources/app/ServiceProvider/FilterProvider.php @@ -0,0 +1,178 @@ +createUserFilter($container); + $this->createProjectFilter($container); + $this->createTaskFilter($container); + return $container; + } + + public function createUserFilter(Container $container) + { + $container['userQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(UserModel::TABLE)); + return $builder; + }); + + return $container; + } + + public function createProjectFilter(Container $container) + { + $container['projectGroupRoleQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(ProjectGroupRoleModel::TABLE)); + return $builder; + }); + + $container['projectUserRoleQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(ProjectUserRoleModel::TABLE)); + return $builder; + }); + + $container['projectQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(ProjectModel::TABLE)); + return $builder; + }); + + $container['projectActivityLexer'] = $container->factory(function ($c) { + $builder = new LexerBuilder(); + $builder + ->withQuery($c['projectActivityModel']->getQuery()) + ->withFilter(new ProjectActivityTaskTitleFilter(), true) + ->withFilter(new ProjectActivityTaskStatusFilter()) + ->withFilter(new ProjectActivityProjectNameFilter()) + ->withFilter(ProjectActivityCreationDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) + ->withFilter(ProjectActivityCreatorFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ) + ; + + return $builder; + }); + + $container['projectActivityQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['projectActivityModel']->getQuery()); + + return $builder; + }); + + return $container; + } + + public function createTaskFilter(Container $container) + { + $container['taskQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['taskFinderModel']->getExtendedQuery()); + return $builder; + }); + + $container['taskLexer'] = $container->factory(function ($c) { + $builder = new LexerBuilder(); + + $builder + ->withQuery($c['taskFinderModel']->getExtendedQuery()) + ->withFilter(TaskAssigneeFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ) + ->withFilter(new TaskCategoryFilter()) + ->withFilter(TaskColorFilter::getInstance() + ->setColorModel($c['colorModel']) + ) + ->withFilter(new TaskColumnFilter()) + ->withFilter(new TaskCommentFilter()) + ->withFilter(TaskCreationDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) + ->withFilter(TaskCreatorFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ) + ->withFilter(new TaskDescriptionFilter()) + ->withFilter(TaskDueDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) + ->withFilter(new TaskIdFilter()) + ->withFilter(TaskLinkFilter::getInstance() + ->setDatabase($c['db']) + ) + ->withFilter(TaskModificationDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) + ->withFilter(new TaskProjectFilter()) + ->withFilter(new TaskReferenceFilter()) + ->withFilter(new TaskStatusFilter()) + ->withFilter(TaskSubtaskAssigneeFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ->setDatabase($c['db']) + ) + ->withFilter(new TaskSwimlaneFilter()) + ->withFilter(TaskTagFilter::getInstance() + ->setDatabase($c['db']) + ) + ->withFilter(new TaskTitleFilter(), true) + ; + + return $builder; + }); + + return $container; + } +} diff --git a/sources/app/ServiceProvider/GroupProvider.php b/sources/app/ServiceProvider/GroupProvider.php index b222b21..08548c7 100644 --- a/sources/app/ServiceProvider/GroupProvider.php +++ b/sources/app/ServiceProvider/GroupProvider.php @@ -11,7 +11,7 @@ use Kanboard\Group\LdapBackendGroupProvider; /** * Group Provider * - * @package serviceProvider + * @package Kanboard\ServiceProvider * @author Frederic Guillot */ class GroupProvider implements ServiceProviderInterface diff --git a/sources/app/ServiceProvider/HelperProvider.php b/sources/app/ServiceProvider/HelperProvider.php index 43a78e3..a909e3c 100644 --- a/sources/app/ServiceProvider/HelperProvider.php +++ b/sources/app/ServiceProvider/HelperProvider.php @@ -7,18 +7,26 @@ use Kanboard\Core\Template; use Pimple\Container; use Pimple\ServiceProviderInterface; +/** + * Class HelperProvider + * + * @package Kanboard\ServiceProvider + * @author Frederic Guillot + */ class HelperProvider implements ServiceProviderInterface { public function register(Container $container) { $container['helper'] = new Helper($container); $container['helper']->register('app', '\Kanboard\Helper\AppHelper'); + $container['helper']->register('calendar', '\Kanboard\Helper\CalendarHelper'); $container['helper']->register('asset', '\Kanboard\Helper\AssetHelper'); $container['helper']->register('board', '\Kanboard\Helper\BoardHelper'); $container['helper']->register('dt', '\Kanboard\Helper\DateHelper'); $container['helper']->register('file', '\Kanboard\Helper\FileHelper'); $container['helper']->register('form', '\Kanboard\Helper\FormHelper'); $container['helper']->register('hook', '\Kanboard\Helper\HookHelper'); + $container['helper']->register('ical', '\Kanboard\Helper\ICalHelper'); $container['helper']->register('layout', '\Kanboard\Helper\LayoutHelper'); $container['helper']->register('model', '\Kanboard\Helper\ModelHelper'); $container['helper']->register('subtask', '\Kanboard\Helper\SubtaskHelper'); @@ -28,6 +36,8 @@ class HelperProvider implements ServiceProviderInterface $container['helper']->register('user', '\Kanboard\Helper\UserHelper'); $container['helper']->register('avatar', '\Kanboard\Helper\AvatarHelper'); $container['helper']->register('projectHeader', '\Kanboard\Helper\ProjectHeaderHelper'); + $container['helper']->register('projectActivity', '\Kanboard\Helper\ProjectActivityHelper'); + $container['helper']->register('mail', '\Kanboard\Helper\MailHelper'); $container['template'] = new Template($container['helper']); diff --git a/sources/app/ServiceProvider/LoggingProvider.php b/sources/app/ServiceProvider/LoggingProvider.php index 68c074f..cb6d0ba 100644 --- a/sources/app/ServiceProvider/LoggingProvider.php +++ b/sources/app/ServiceProvider/LoggingProvider.php @@ -6,27 +6,48 @@ use Psr\Log\LogLevel; use Pimple\Container; use Pimple\ServiceProviderInterface; use SimpleLogger\Logger; +use SimpleLogger\Stderr; +use SimpleLogger\Stdout; use SimpleLogger\Syslog; use SimpleLogger\File; +/** + * Class LoggingProvider + * + * @package Kanboard\ServiceProvider + * @author Frederic Guillot + */ class LoggingProvider implements ServiceProviderInterface { public function register(Container $container) { $logger = new Logger; + $driver = null; - if (ENABLE_SYSLOG) { - $syslog = new Syslog('kanboard'); - $syslog->setLevel(LogLevel::ERROR); - $logger->setLogger($syslog); + switch (LOG_DRIVER) { + case 'syslog': + $driver = new Syslog('kanboard'); + break; + case 'stdout': + $driver = new Stdout(); + break; + case 'stderr': + $driver = new Stderr(); + break; + case 'file': + $driver = new File(LOG_FILE); + break; } - if (DEBUG) { - $logger->setLogger(new File(DEBUG_FILE)); + if ($driver !== null) { + if (! DEBUG) { + $driver->setLevel(LogLevel::INFO); + } + + $logger->setLogger($driver); } $container['logger'] = $logger; - return $container; } } diff --git a/sources/app/ServiceProvider/MailProvider.php b/sources/app/ServiceProvider/MailProvider.php new file mode 100644 index 0000000..685709e --- /dev/null +++ b/sources/app/ServiceProvider/MailProvider.php @@ -0,0 +1,34 @@ +setTransport('smtp', '\Kanboard\Core\Mail\Transport\Smtp'); + $mailer->setTransport('sendmail', '\Kanboard\Core\Mail\Transport\Sendmail'); + $mailer->setTransport('mail', '\Kanboard\Core\Mail\Transport\Mail'); + return $mailer; + }; + + return $container; + } +} diff --git a/sources/app/ServiceProvider/NotificationProvider.php b/sources/app/ServiceProvider/NotificationProvider.php index 83daf65..a057120 100644 --- a/sources/app/ServiceProvider/NotificationProvider.php +++ b/sources/app/ServiceProvider/NotificationProvider.php @@ -4,15 +4,15 @@ namespace Kanboard\ServiceProvider; use Pimple\Container; use Pimple\ServiceProviderInterface; -use Kanboard\Model\UserNotificationType; -use Kanboard\Model\ProjectNotificationType; -use Kanboard\Notification\Mail as MailNotification; -use Kanboard\Notification\Web as WebNotification; +use Kanboard\Model\UserNotificationTypeModel; +use Kanboard\Model\ProjectNotificationTypeModel; +use Kanboard\Notification\MailNotification as MailNotification; +use Kanboard\Notification\WebNotification as WebNotification; /** * Notification Provider * - * @package serviceProvider + * @package Kanboard\ServiceProvider * @author Frederic Guillot */ class NotificationProvider implements ServiceProviderInterface @@ -26,17 +26,17 @@ class NotificationProvider implements ServiceProviderInterface */ public function register(Container $container) { - $container['userNotificationType'] = function ($container) { - $type = new UserNotificationType($container); - $type->setType(MailNotification::TYPE, t('Email'), '\Kanboard\Notification\Mail'); - $type->setType(WebNotification::TYPE, t('Web'), '\Kanboard\Notification\Web'); + $container['userNotificationTypeModel'] = function ($container) { + $type = new UserNotificationTypeModel($container); + $type->setType(MailNotification::TYPE, t('Email'), '\Kanboard\Notification\MailNotification'); + $type->setType(WebNotification::TYPE, t('Web'), '\Kanboard\Notification\WebNotification'); return $type; }; - $container['projectNotificationType'] = function ($container) { - $type = new ProjectNotificationType($container); - $type->setType('webhook', 'Webhook', '\Kanboard\Notification\Webhook', true); - $type->setType('activity_stream', 'ActivityStream', '\Kanboard\Notification\ActivityStream', true); + $container['projectNotificationTypeModel'] = function ($container) { + $type = new ProjectNotificationTypeModel($container); + $type->setType('webhook', 'Webhook', '\Kanboard\Notification\WebhookNotification', true); + $type->setType('activity_stream', 'ActivityStream', '\Kanboard\Notification\ActivityStreamNotification', true); return $type; }; diff --git a/sources/app/ServiceProvider/PluginProvider.php b/sources/app/ServiceProvider/PluginProvider.php index d2f1666..4cf5725 100644 --- a/sources/app/ServiceProvider/PluginProvider.php +++ b/sources/app/ServiceProvider/PluginProvider.php @@ -9,7 +9,7 @@ use Kanboard\Core\Plugin\Loader; /** * Plugin Provider * - * @package serviceProvider + * @package Kanboard\ServiceProvider * @author Frederic Guillot */ class PluginProvider implements ServiceProviderInterface diff --git a/sources/app/ServiceProvider/QueueProvider.php b/sources/app/ServiceProvider/QueueProvider.php new file mode 100644 index 0000000..946b436 --- /dev/null +++ b/sources/app/ServiceProvider/QueueProvider.php @@ -0,0 +1,27 @@ +enable(); // Dashboard - $container['route']->addRoute('dashboard', 'app', 'index'); - $container['route']->addRoute('dashboard/:user_id', 'app', 'index'); - $container['route']->addRoute('dashboard/:user_id/projects', 'app', 'projects'); - $container['route']->addRoute('dashboard/:user_id/tasks', 'app', 'tasks'); - $container['route']->addRoute('dashboard/:user_id/subtasks', 'app', 'subtasks'); - $container['route']->addRoute('dashboard/:user_id/calendar', 'app', 'calendar'); - $container['route']->addRoute('dashboard/:user_id/activity', 'app', 'activity'); - $container['route']->addRoute('dashboard/:user_id/notifications', 'app', 'notifications'); + $container['route']->addRoute('dashboard', 'DashboardController', 'show'); + $container['route']->addRoute('dashboard/:user_id', 'DashboardController', 'show'); + $container['route']->addRoute('dashboard/:user_id/projects', 'DashboardController', 'projects'); + $container['route']->addRoute('dashboard/:user_id/tasks', 'DashboardController', 'tasks'); + $container['route']->addRoute('dashboard/:user_id/subtasks', 'DashboardController', 'subtasks'); + $container['route']->addRoute('dashboard/:user_id/calendar', 'DashboardController', 'calendar'); + $container['route']->addRoute('dashboard/:user_id/activity', 'DashboardController', 'activity'); + $container['route']->addRoute('dashboard/:user_id/notifications', 'DashboardController', 'notifications'); // Search routes - $container['route']->addRoute('search', 'search', 'index'); - $container['route']->addRoute('search/:search', 'search', 'index'); + $container['route']->addRoute('search', 'SearchController', 'index'); + $container['route']->addRoute('search/activity', 'SearchController', 'activity'); // ProjectCreation routes - $container['route']->addRoute('project/create', 'ProjectCreation', 'create'); - $container['route']->addRoute('project/create/private', 'ProjectCreation', 'createPrivate'); + $container['route']->addRoute('project/create', 'ProjectCreationController', 'create'); + $container['route']->addRoute('project/create/private', 'ProjectCreationController', 'createPrivate'); // Project routes - $container['route']->addRoute('projects', 'project', 'index'); - $container['route']->addRoute('project/:project_id', 'project', 'show'); - $container['route']->addRoute('p/:project_id', 'project', 'show'); - $container['route']->addRoute('project/:project_id/customer-filters', 'customfilter', 'index'); - $container['route']->addRoute('project/:project_id/share', 'project', 'share'); - $container['route']->addRoute('project/:project_id/notifications', 'project', 'notifications'); - $container['route']->addRoute('project/:project_id/integrations', 'project', 'integrations'); - $container['route']->addRoute('project/:project_id/duplicate', 'project', 'duplicate'); - $container['route']->addRoute('project/:project_id/remove', 'project', 'remove'); - $container['route']->addRoute('project/:project_id/disable', 'project', 'disable'); - $container['route']->addRoute('project/:project_id/enable', 'project', 'enable'); - $container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index'); - $container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1'); + $container['route']->addRoute('projects', 'ProjectListController', 'show'); + $container['route']->addRoute('project/:project_id', 'ProjectViewController', 'show'); + $container['route']->addRoute('p/:project_id', 'ProjectViewController', 'show'); + $container['route']->addRoute('project/:project_id/customer-filters', 'CustomFilterController', 'index'); + $container['route']->addRoute('project/:project_id/share', 'ProjectViewController', 'share'); + $container['route']->addRoute('project/:project_id/notifications', 'ProjectViewController', 'notifications'); + $container['route']->addRoute('project/:project_id/integrations', 'ProjectViewController', 'integrations'); + $container['route']->addRoute('project/:project_id/duplicate', 'ProjectViewController', 'duplicate'); + $container['route']->addRoute('project/:project_id/permissions', 'ProjectPermissionController', 'index'); + $container['route']->addRoute('project/:project_id/activity', 'ActivityController', 'project'); + $container['route']->addRoute('project/:project_id/tags', 'ProjectTagController', 'index'); // Project Overview - $container['route']->addRoute('project/:project_id/overview', 'ProjectOverview', 'show'); + $container['route']->addRoute('project/:project_id/overview', 'ProjectOverviewController', 'show'); // ProjectEdit routes - $container['route']->addRoute('project/:project_id/edit', 'ProjectEdit', 'edit'); - $container['route']->addRoute('project/:project_id/edit/dates', 'ProjectEdit', 'dates'); - $container['route']->addRoute('project/:project_id/edit/description', 'ProjectEdit', 'description'); - $container['route']->addRoute('project/:project_id/edit/priority', 'ProjectEdit', 'priority'); + $container['route']->addRoute('project/:project_id/edit', 'ProjectEditController', 'edit'); + $container['route']->addRoute('project/:project_id/edit/dates', 'ProjectEditController', 'dates'); + $container['route']->addRoute('project/:project_id/edit/description', 'ProjectEditController', 'description'); + $container['route']->addRoute('project/:project_id/edit/priority', 'ProjectEditController', 'priority'); // ProjectUser routes - $container['route']->addRoute('projects/managers/:user_id', 'projectuser', 'managers'); - $container['route']->addRoute('projects/members/:user_id', 'projectuser', 'members'); - $container['route']->addRoute('projects/tasks/:user_id/opens', 'projectuser', 'opens'); - $container['route']->addRoute('projects/tasks/:user_id/closed', 'projectuser', 'closed'); - $container['route']->addRoute('projects/managers', 'projectuser', 'managers'); + $container['route']->addRoute('projects/managers/:user_id', 'ProjectUserOverviewController', 'managers'); + $container['route']->addRoute('projects/members/:user_id', 'ProjectUserOverviewController', 'members'); + $container['route']->addRoute('projects/tasks/:user_id/opens', 'ProjectUserOverviewController', 'opens'); + $container['route']->addRoute('projects/tasks/:user_id/closed', 'ProjectUserOverviewController', 'closed'); + $container['route']->addRoute('projects/managers', 'ProjectUserOverviewController', 'managers'); // Action routes - $container['route']->addRoute('project/:project_id/actions', 'action', 'index'); + $container['route']->addRoute('project/:project_id/actions', 'ActionController', 'index'); // Column routes - $container['route']->addRoute('project/:project_id/columns', 'column', 'index'); + $container['route']->addRoute('project/:project_id/columns', 'ColumnController', 'index'); // Swimlane routes - $container['route']->addRoute('project/:project_id/swimlanes', 'swimlane', 'index'); + $container['route']->addRoute('project/:project_id/swimlanes', 'SwimlaneController', 'index'); // Category routes - $container['route']->addRoute('project/:project_id/categories', 'category', 'index'); + $container['route']->addRoute('project/:project_id/categories', 'CategoryController', 'index'); + + // Import routes + $container['route']->addRoute('project/:project_id/import', 'TaskImportController', 'show'); // Task routes - $container['route']->addRoute('project/:project_id/task/:task_id', 'task', 'show'); - $container['route']->addRoute('t/:task_id', 'task', 'show'); - $container['route']->addRoute('public/task/:task_id/:token', 'task', 'readonly'); + $container['route']->addRoute('project/:project_id/task/:task_id', 'TaskViewController', 'show'); + $container['route']->addRoute('t/:task_id', 'TaskViewController', 'show'); + $container['route']->addRoute('public/task/:task_id/:token', 'TaskViewController', 'readonly'); - $container['route']->addRoute('project/:project_id/task/:task_id/activity', 'activity', 'task'); - $container['route']->addRoute('project/:project_id/task/:task_id/transitions', 'task', 'transitions'); - $container['route']->addRoute('project/:project_id/task/:task_id/analytics', 'task', 'analytics'); - $container['route']->addRoute('project/:project_id/task/:task_id/subtasks', 'subtask', 'show'); - $container['route']->addRoute('project/:project_id/task/:task_id/time-tracking', 'task', 'timetracking'); + $container['route']->addRoute('project/:project_id/task/:task_id/activity', 'ActivityController', 'task'); + $container['route']->addRoute('project/:project_id/task/:task_id/transitions', 'TaskViewController', 'transitions'); + $container['route']->addRoute('project/:project_id/task/:task_id/analytics', 'TaskViewController', 'analytics'); + $container['route']->addRoute('project/:project_id/task/:task_id/time-tracking', 'TaskViewController', 'timetracking'); // Exports - $container['route']->addRoute('export/tasks/:project_id', 'export', 'tasks'); - $container['route']->addRoute('export/subtasks/:project_id', 'export', 'subtasks'); - $container['route']->addRoute('export/transitions/:project_id', 'export', 'transitions'); - $container['route']->addRoute('export/summary/:project_id', 'export', 'summary'); + $container['route']->addRoute('export/tasks/:project_id', 'ExportController', 'tasks'); + $container['route']->addRoute('export/subtasks/:project_id', 'ExportController', 'subtasks'); + $container['route']->addRoute('export/transitions/:project_id', 'ExportController', 'transitions'); + $container['route']->addRoute('export/summary/:project_id', 'ExportController', 'summary'); // Analytics routes - $container['route']->addRoute('analytics/tasks/:project_id', 'analytic', 'tasks'); - $container['route']->addRoute('analytics/users/:project_id', 'analytic', 'users'); - $container['route']->addRoute('analytics/cfd/:project_id', 'analytic', 'cfd'); - $container['route']->addRoute('analytics/burndown/:project_id', 'analytic', 'burndown'); - $container['route']->addRoute('analytics/average-time-column/:project_id', 'analytic', 'averageTimeByColumn'); - $container['route']->addRoute('analytics/lead-cycle-time/:project_id', 'analytic', 'leadAndCycleTime'); - $container['route']->addRoute('analytics/estimated-spent-time/:project_id', 'analytic', 'compareHours'); + $container['route']->addRoute('analytics/tasks/:project_id', 'AnalyticController', 'tasks'); + $container['route']->addRoute('analytics/users/:project_id', 'AnalyticController', 'users'); + $container['route']->addRoute('analytics/cfd/:project_id', 'AnalyticController', 'cfd'); + $container['route']->addRoute('analytics/burndown/:project_id', 'AnalyticController', 'burndown'); + $container['route']->addRoute('analytics/average-time-column/:project_id', 'AnalyticController', 'averageTimeByColumn'); + $container['route']->addRoute('analytics/lead-cycle-time/:project_id', 'AnalyticController', 'leadAndCycleTime'); + $container['route']->addRoute('analytics/estimated-spent-time/:project_id', 'AnalyticController', 'compareHours'); // Board routes - $container['route']->addRoute('board/:project_id', 'board', 'show'); - $container['route']->addRoute('b/:project_id', 'board', 'show'); - $container['route']->addRoute('public/board/:token', 'board', 'readonly'); + $container['route']->addRoute('board/:project_id', 'BoardViewController', 'show'); + $container['route']->addRoute('b/:project_id', 'BoardViewController', 'show'); + $container['route']->addRoute('public/board/:token', 'BoardViewController', 'readonly'); // Calendar routes - $container['route']->addRoute('calendar/:project_id', 'calendar', 'show'); - $container['route']->addRoute('c/:project_id', 'calendar', 'show'); + $container['route']->addRoute('calendar/:project_id', 'CalendarController', 'show'); + $container['route']->addRoute('c/:project_id', 'CalendarController', 'show'); // Listing routes - $container['route']->addRoute('list/:project_id', 'listing', 'show'); - $container['route']->addRoute('l/:project_id', 'listing', 'show'); + $container['route']->addRoute('list/:project_id', 'TaskListController', 'show'); + $container['route']->addRoute('l/:project_id', 'TaskListController', 'show'); // Gantt routes - $container['route']->addRoute('gantt/:project_id', 'gantt', 'project'); - $container['route']->addRoute('gantt/:project_id/sort/:sorting', 'gantt', 'project'); + $container['route']->addRoute('gantt/:project_id', 'TaskGanttController', 'show'); + $container['route']->addRoute('gantt/:project_id/sort/:sorting', 'TaskGanttController', 'show'); // Feed routes - $container['route']->addRoute('feed/project/:token', 'feed', 'project'); - $container['route']->addRoute('feed/user/:token', 'feed', 'user'); + $container['route']->addRoute('feed/project/:token', 'FeedController', 'project'); + $container['route']->addRoute('feed/user/:token', 'FeedController', 'user'); // Ical routes - $container['route']->addRoute('ical/project/:token', 'ical', 'project'); - $container['route']->addRoute('ical/user/:token', 'ical', 'user'); + $container['route']->addRoute('ical/project/:token', 'ICalendarController', 'project'); + $container['route']->addRoute('ical/user/:token', 'ICalendarController', 'user'); // Users - $container['route']->addRoute('users', 'user', 'index'); - $container['route']->addRoute('user/profile/:user_id', 'user', 'profile'); - $container['route']->addRoute('user/show/:user_id', 'user', 'show'); - $container['route']->addRoute('user/show/:user_id/timesheet', 'user', 'timesheet'); - $container['route']->addRoute('user/show/:user_id/last-logins', 'user', 'last'); - $container['route']->addRoute('user/show/:user_id/sessions', 'user', 'sessions'); - $container['route']->addRoute('user/:user_id/edit', 'user', 'edit'); - $container['route']->addRoute('user/:user_id/password', 'user', 'password'); - $container['route']->addRoute('user/:user_id/share', 'user', 'share'); - $container['route']->addRoute('user/:user_id/notifications', 'user', 'notifications'); - $container['route']->addRoute('user/:user_id/accounts', 'user', 'external'); - $container['route']->addRoute('user/:user_id/integrations', 'user', 'integrations'); - $container['route']->addRoute('user/:user_id/authentication', 'user', 'authentication'); - $container['route']->addRoute('user/:user_id/2fa', 'twofactor', 'index'); + $container['route']->addRoute('users', 'UserListController', 'show'); + $container['route']->addRoute('user/profile/:user_id', 'UserViewController', 'profile'); + $container['route']->addRoute('user/show/:user_id', 'UserViewController', 'show'); + $container['route']->addRoute('user/show/:user_id/timesheet', 'UserViewController', 'timesheet'); + $container['route']->addRoute('user/show/:user_id/last-logins', 'UserViewController', 'lastLogin'); + $container['route']->addRoute('user/show/:user_id/sessions', 'UserViewController', 'sessions'); + $container['route']->addRoute('user/:user_id/edit', 'UserModificationController', 'show'); + $container['route']->addRoute('user/:user_id/password', 'UserCredentialController', 'changePassword'); + $container['route']->addRoute('user/:user_id/share', 'UserViewController', 'share'); + $container['route']->addRoute('user/:user_id/notifications', 'UserViewController', 'notifications'); + $container['route']->addRoute('user/:user_id/accounts', 'UserViewController', 'external'); + $container['route']->addRoute('user/:user_id/integrations', 'UserViewController', 'integrations'); + $container['route']->addRoute('user/:user_id/authentication', 'UserCredentialController', 'changeAuthentication'); + $container['route']->addRoute('user/:user_id/2fa', 'TwoFactorController', 'index'); + $container['route']->addRoute('user/:user_id/avatar', 'AvatarFileController', 'show'); // Groups - $container['route']->addRoute('groups', 'group', 'index'); - $container['route']->addRoute('groups/create', 'group', 'create'); - $container['route']->addRoute('group/:group_id/associate', 'group', 'associate'); - $container['route']->addRoute('group/:group_id/dissociate/:user_id', 'group', 'dissociate'); - $container['route']->addRoute('group/:group_id/edit', 'group', 'edit'); - $container['route']->addRoute('group/:group_id/members', 'group', 'users'); - $container['route']->addRoute('group/:group_id/remove', 'group', 'confirm'); + $container['route']->addRoute('groups', 'GroupListController', 'index'); + $container['route']->addRoute('group/:group_id/members', 'GroupListController', 'users'); // Config - $container['route']->addRoute('settings', 'config', 'index'); - $container['route']->addRoute('settings/plugins', 'config', 'plugins'); - $container['route']->addRoute('settings/application', 'config', 'application'); - $container['route']->addRoute('settings/project', 'config', 'project'); - $container['route']->addRoute('settings/project', 'config', 'project'); - $container['route']->addRoute('settings/board', 'config', 'board'); - $container['route']->addRoute('settings/calendar', 'config', 'calendar'); - $container['route']->addRoute('settings/integrations', 'config', 'integrations'); - $container['route']->addRoute('settings/webhook', 'config', 'webhook'); - $container['route']->addRoute('settings/api', 'config', 'api'); - $container['route']->addRoute('settings/links', 'link', 'index'); - $container['route']->addRoute('settings/currencies', 'currency', 'index'); + $container['route']->addRoute('settings', 'ConfigController', 'index'); + $container['route']->addRoute('settings/application', 'ConfigController', 'application'); + $container['route']->addRoute('settings/project', 'ConfigController', 'project'); + $container['route']->addRoute('settings/project', 'ConfigController', 'project'); + $container['route']->addRoute('settings/board', 'ConfigController', 'board'); + $container['route']->addRoute('settings/calendar', 'ConfigController', 'calendar'); + $container['route']->addRoute('settings/integrations', 'ConfigController', 'integrations'); + $container['route']->addRoute('settings/webhook', 'ConfigController', 'webhook'); + $container['route']->addRoute('settings/api', 'ConfigController', 'api'); + $container['route']->addRoute('settings/links', 'LinkController', 'index'); + $container['route']->addRoute('settings/currencies', 'CurrencyController', 'index'); + $container['route']->addRoute('settings/tags', 'TagController', 'index'); + + // Plugins + $container['route']->addRoute('extensions', 'PluginController', 'show'); + $container['route']->addRoute('extensions/directory', 'PluginController', 'directory'); // Doc - $container['route']->addRoute('documentation/:file', 'doc', 'show'); - $container['route']->addRoute('documentation', 'doc', 'show'); + $container['route']->addRoute('documentation/:file', 'DocumentationController', 'show'); + $container['route']->addRoute('documentation', 'DocumentationController', 'show'); // Auth routes - $container['route']->addRoute('login', 'auth', 'login'); - $container['route']->addRoute('logout', 'auth', 'logout'); + $container['route']->addRoute('login', 'AuthController', 'login'); + $container['route']->addRoute('logout', 'AuthController', 'logout'); // PasswordReset - $container['route']->addRoute('forgot-password', 'PasswordReset', 'create'); - $container['route']->addRoute('forgot-password/change/:token', 'PasswordReset', 'change'); + $container['route']->addRoute('forgot-password', 'PasswordResetController', 'create'); + $container['route']->addRoute('forgot-password/change/:token', 'PasswordResetController', 'change'); } return $container; diff --git a/sources/app/ServiceProvider/SessionProvider.php b/sources/app/ServiceProvider/SessionProvider.php index 0999d53..96dcac2 100644 --- a/sources/app/ServiceProvider/SessionProvider.php +++ b/sources/app/ServiceProvider/SessionProvider.php @@ -11,7 +11,7 @@ use Kanboard\Core\Session\FlashMessage; /** * Session Provider * - * @package serviceProvider + * @package Kanboard\ServiceProvider * @author Frederic Guillot */ class SessionProvider implements ServiceProviderInterface diff --git a/sources/app/Subscriber/AuthSubscriber.php b/sources/app/Subscriber/AuthSubscriber.php index dfb95a0..0097c40 100644 --- a/sources/app/Subscriber/AuthSubscriber.php +++ b/sources/app/Subscriber/AuthSubscriber.php @@ -45,9 +45,9 @@ class AuthSubscriber extends BaseSubscriber implements EventSubscriberInterface $userAgent = $this->request->getUserAgent(); $ipAddress = $this->request->getIpAddress(); - $this->userLocking->resetFailedLogin($this->userSession->getUsername()); + $this->userLockingModel->resetFailedLogin($this->userSession->getUsername()); - $this->lastLogin->create( + $this->lastLoginModel->create( $event->getAuthType(), $this->userSession->getId(), $ipAddress, @@ -59,7 +59,7 @@ class AuthSubscriber extends BaseSubscriber implements EventSubscriberInterface } if (isset($this->sessionStorage->hasRememberMe) && $this->sessionStorage->hasRememberMe) { - $session = $this->rememberMeSession->create($this->userSession->getId(), $ipAddress, $userAgent); + $session = $this->rememberMeSessionModel->create($this->userSession->getId(), $ipAddress, $userAgent); $this->rememberMeCookie->write($session['token'], $session['sequence'], $session['expiration']); } } @@ -75,10 +75,10 @@ class AuthSubscriber extends BaseSubscriber implements EventSubscriberInterface $credentials = $this->rememberMeCookie->read(); if ($credentials !== false) { - $session = $this->rememberMeSession->find($credentials['token'], $credentials['sequence']); + $session = $this->rememberMeSessionModel->find($credentials['token'], $credentials['sequence']); if (! empty($session)) { - $this->rememberMeSession->remove($session['id']); + $this->rememberMeSessionModel->remove($session['id']); } $this->rememberMeCookie->remove(); @@ -97,10 +97,10 @@ class AuthSubscriber extends BaseSubscriber implements EventSubscriberInterface $username = $event->getUsername(); if (! empty($username)) { - $this->userLocking->incrementFailedLogin($username); + $this->userLockingModel->incrementFailedLogin($username); - if ($this->userLocking->getFailedLogin($username) > BRUTEFORCE_LOCKDOWN) { - $this->userLocking->lock($username, BRUTEFORCE_LOCKDOWN_DURATION); + if ($this->userLockingModel->getFailedLogin($username) > BRUTEFORCE_LOCKDOWN) { + $this->userLockingModel->lock($username, BRUTEFORCE_LOCKDOWN_DURATION); } } } diff --git a/sources/app/Subscriber/BaseSubscriber.php b/sources/app/Subscriber/BaseSubscriber.php index 2e41da7..fdea29f 100644 --- a/sources/app/Subscriber/BaseSubscriber.php +++ b/sources/app/Subscriber/BaseSubscriber.php @@ -34,7 +34,6 @@ class BaseSubscriber extends Base } $this->called[$key] = true; - return false; } } diff --git a/sources/app/Subscriber/BootstrapSubscriber.php b/sources/app/Subscriber/BootstrapSubscriber.php index ef0215f..7d12e9a 100644 --- a/sources/app/Subscriber/BootstrapSubscriber.php +++ b/sources/app/Subscriber/BootstrapSubscriber.php @@ -16,12 +16,12 @@ class BootstrapSubscriber extends BaseSubscriber implements EventSubscriberInter public function execute() { $this->logger->debug('Subscriber executed: '.__METHOD__); - $this->config->setupTranslations(); - $this->config->setupTimezone(); + $this->languageModel->loadCurrentLanguage(); + $this->timezoneModel->setCurrentTimezone(); $this->actionManager->attachEvents(); if ($this->userSession->isLogged()) { - $this->sessionStorage->hasSubtaskInProgress = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); + $this->sessionStorage->hasSubtaskInProgress = $this->subtaskModel->hasSubtaskInProgress($this->userSession->getId()); } } @@ -29,13 +29,13 @@ class BootstrapSubscriber extends BaseSubscriber implements EventSubscriberInter { if (DEBUG) { foreach ($this->db->getLogMessages() as $message) { - $this->logger->debug($message); + $this->logger->debug('SQL: ' . $message); } - $this->logger->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries)); - $this->logger->debug('RENDERING={time}', array('time' => microtime(true) - $this->request->getStartTime())); - $this->logger->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage())); - $this->logger->debug('URI='.$this->request->getUri()); + $this->logger->debug('APP: nb_queries={nb}', array('nb' => $this->db->getStatementHandler()->getNbQueries())); + $this->logger->debug('APP: rendering_time={time}', array('time' => microtime(true) - $this->request->getStartTime())); + $this->logger->debug('APP: memory_usage='.$this->helper->text->bytes(memory_get_usage())); + $this->logger->debug('APP: uri='.$this->request->getUri()); $this->logger->debug('###############################################'); } } diff --git a/sources/app/Subscriber/LdapUserPhotoSubscriber.php b/sources/app/Subscriber/LdapUserPhotoSubscriber.php new file mode 100644 index 0000000..93672cd --- /dev/null +++ b/sources/app/Subscriber/LdapUserPhotoSubscriber.php @@ -0,0 +1,49 @@ + 'syncUserPhoto', + ); + } + + /** + * Save the user profile photo from LDAP to the object storage + * + * @access public + * @param UserProfileSyncEvent $event + */ + public function syncUserPhoto(UserProfileSyncEvent $event) + { + if (is_a($event->getUser(), 'Kanboard\User\LdapUserProvider')) { + $profile = $event->getProfile(); + $photo = $event->getUser()->getPhoto(); + + if (empty($profile['avatar_path']) && ! empty($photo)) { + $this->logger->info('Saving user photo from LDAP profile'); + $this->avatarFileModel->uploadImageContent($profile['id'], $photo); + } + } + } +} diff --git a/sources/app/Subscriber/NotificationSubscriber.php b/sources/app/Subscriber/NotificationSubscriber.php index 651b8a9..db11e58 100644 --- a/sources/app/Subscriber/NotificationSubscriber.php +++ b/sources/app/Subscriber/NotificationSubscriber.php @@ -3,10 +3,11 @@ namespace Kanboard\Subscriber; use Kanboard\Event\GenericEvent; -use Kanboard\Model\Task; -use Kanboard\Model\Comment; -use Kanboard\Model\Subtask; -use Kanboard\Model\TaskFile; +use Kanboard\Job\NotificationJob; +use Kanboard\Model\TaskModel; +use Kanboard\Model\CommentModel; +use Kanboard\Model\SubtaskModel; +use Kanboard\Model\TaskFileModel; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class NotificationSubscriber extends BaseSubscriber implements EventSubscriberInterface @@ -14,67 +15,32 @@ class NotificationSubscriber extends BaseSubscriber implements EventSubscriberIn public static function getSubscribedEvents() { return array( - Task::EVENT_USER_MENTION => 'handleEvent', - Task::EVENT_CREATE => 'handleEvent', - Task::EVENT_UPDATE => 'handleEvent', - Task::EVENT_CLOSE => 'handleEvent', - Task::EVENT_OPEN => 'handleEvent', - Task::EVENT_MOVE_COLUMN => 'handleEvent', - Task::EVENT_MOVE_POSITION => 'handleEvent', - Task::EVENT_MOVE_SWIMLANE => 'handleEvent', - Task::EVENT_ASSIGNEE_CHANGE => 'handleEvent', - Subtask::EVENT_CREATE => 'handleEvent', - Subtask::EVENT_UPDATE => 'handleEvent', - Comment::EVENT_CREATE => 'handleEvent', - Comment::EVENT_UPDATE => 'handleEvent', - Comment::EVENT_USER_MENTION => 'handleEvent', - TaskFile::EVENT_CREATE => 'handleEvent', + TaskModel::EVENT_USER_MENTION => 'handleEvent', + TaskModel::EVENT_CREATE => 'handleEvent', + TaskModel::EVENT_UPDATE => 'handleEvent', + TaskModel::EVENT_CLOSE => 'handleEvent', + TaskModel::EVENT_OPEN => 'handleEvent', + TaskModel::EVENT_MOVE_COLUMN => 'handleEvent', + TaskModel::EVENT_MOVE_POSITION => 'handleEvent', + TaskModel::EVENT_MOVE_SWIMLANE => 'handleEvent', + TaskModel::EVENT_ASSIGNEE_CHANGE => 'handleEvent', + SubtaskModel::EVENT_CREATE => 'handleEvent', + SubtaskModel::EVENT_UPDATE => 'handleEvent', + CommentModel::EVENT_CREATE => 'handleEvent', + CommentModel::EVENT_UPDATE => 'handleEvent', + CommentModel::EVENT_USER_MENTION => 'handleEvent', + TaskFileModel::EVENT_CREATE => 'handleEvent', ); } - public function handleEvent(GenericEvent $event, $event_name) + public function handleEvent(GenericEvent $event, $eventName) { - if (! $this->isExecuted($event_name)) { - $this->logger->debug('Subscriber executed: '.__METHOD__); - $event_data = $this->getEventData($event); + if (!$this->isExecuted($eventName)) { + $this->logger->debug('Subscriber executed: ' . __METHOD__); - if (! empty($event_data)) { - if (! empty($event['mention'])) { - $this->userNotification->sendUserNotification($event['mention'], $event_name, $event_data); - } else { - $this->userNotification->sendNotifications($event_name, $event_data); - $this->projectNotification->sendNotifications($event_data['task']['project_id'], $event_name, $event_data); - } - } + $this->queueManager->push(NotificationJob::getInstance($this->container) + ->withParams($event, $eventName, get_class($event)) + ); } } - - public function getEventData(GenericEvent $event) - { - $values = array(); - - if (! empty($event['changes'])) { - $values['changes'] = $event['changes']; - } - - switch (get_class($event)) { - case 'Kanboard\Event\TaskEvent': - $values['task'] = $this->taskFinder->getDetails($event['task_id']); - break; - case 'Kanboard\Event\SubtaskEvent': - $values['subtask'] = $this->subtask->getById($event['id'], true); - $values['task'] = $this->taskFinder->getDetails($values['subtask']['task_id']); - break; - case 'Kanboard\Event\FileEvent': - $values['file'] = $event->getAll(); - $values['task'] = $this->taskFinder->getDetails($values['file']['task_id']); - break; - case 'Kanboard\Event\CommentEvent': - $values['comment'] = $this->comment->getById($event['id']); - $values['task'] = $this->taskFinder->getDetails($values['comment']['task_id']); - break; - } - - return $values; - } } diff --git a/sources/app/Subscriber/ProjectDailySummarySubscriber.php b/sources/app/Subscriber/ProjectDailySummarySubscriber.php index 44138f4..6971a12 100644 --- a/sources/app/Subscriber/ProjectDailySummarySubscriber.php +++ b/sources/app/Subscriber/ProjectDailySummarySubscriber.php @@ -3,7 +3,8 @@ namespace Kanboard\Subscriber; use Kanboard\Event\TaskEvent; -use Kanboard\Model\Task; +use Kanboard\Job\ProjectMetricJob; +use Kanboard\Model\TaskModel; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubscriberInterface @@ -11,11 +12,11 @@ class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubsc public static function getSubscribedEvents() { return array( - Task::EVENT_CREATE_UPDATE => 'execute', - Task::EVENT_CLOSE => 'execute', - Task::EVENT_OPEN => 'execute', - Task::EVENT_MOVE_COLUMN => 'execute', - Task::EVENT_MOVE_SWIMLANE => 'execute', + TaskModel::EVENT_CREATE_UPDATE => 'execute', + TaskModel::EVENT_CLOSE => 'execute', + TaskModel::EVENT_OPEN => 'execute', + TaskModel::EVENT_MOVE_COLUMN => 'execute', + TaskModel::EVENT_MOVE_SWIMLANE => 'execute', ); } @@ -23,8 +24,7 @@ class ProjectDailySummarySubscriber extends BaseSubscriber implements EventSubsc { if (isset($event['project_id']) && !$this->isExecuted()) { $this->logger->debug('Subscriber executed: '.__METHOD__); - $this->projectDailyColumnStats->updateTotals($event['project_id'], date('Y-m-d')); - $this->projectDailyStats->updateTotals($event['project_id'], date('Y-m-d')); + $this->queueManager->push(ProjectMetricJob::getInstance($this->container)->withParams($event['project_id'])); } } } diff --git a/sources/app/Subscriber/ProjectModificationDateSubscriber.php b/sources/app/Subscriber/ProjectModificationDateSubscriber.php index 62804a8..fee04ea 100644 --- a/sources/app/Subscriber/ProjectModificationDateSubscriber.php +++ b/sources/app/Subscriber/ProjectModificationDateSubscriber.php @@ -3,7 +3,7 @@ namespace Kanboard\Subscriber; use Kanboard\Event\GenericEvent; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ProjectModificationDateSubscriber extends BaseSubscriber implements EventSubscriberInterface @@ -11,14 +11,14 @@ class ProjectModificationDateSubscriber extends BaseSubscriber implements EventS public static function getSubscribedEvents() { return array( - Task::EVENT_CREATE_UPDATE => 'execute', - Task::EVENT_CLOSE => 'execute', - Task::EVENT_OPEN => 'execute', - Task::EVENT_MOVE_SWIMLANE => 'execute', - Task::EVENT_MOVE_COLUMN => 'execute', - Task::EVENT_MOVE_POSITION => 'execute', - Task::EVENT_MOVE_PROJECT => 'execute', - Task::EVENT_ASSIGNEE_CHANGE => 'execute', + TaskModel::EVENT_CREATE_UPDATE => 'execute', + TaskModel::EVENT_CLOSE => 'execute', + TaskModel::EVENT_OPEN => 'execute', + TaskModel::EVENT_MOVE_SWIMLANE => 'execute', + TaskModel::EVENT_MOVE_COLUMN => 'execute', + TaskModel::EVENT_MOVE_POSITION => 'execute', + TaskModel::EVENT_MOVE_PROJECT => 'execute', + TaskModel::EVENT_ASSIGNEE_CHANGE => 'execute', ); } @@ -26,7 +26,7 @@ class ProjectModificationDateSubscriber extends BaseSubscriber implements EventS { if (isset($event['project_id']) && !$this->isExecuted()) { $this->logger->debug('Subscriber executed: '.__METHOD__); - $this->project->updateModificationDate($event['project_id']); + $this->projectModel->updateModificationDate($event['project_id']); } } } diff --git a/sources/app/Subscriber/RecurringTaskSubscriber.php b/sources/app/Subscriber/RecurringTaskSubscriber.php index 09a5665..21cd399 100644 --- a/sources/app/Subscriber/RecurringTaskSubscriber.php +++ b/sources/app/Subscriber/RecurringTaskSubscriber.php @@ -3,7 +3,7 @@ namespace Kanboard\Subscriber; use Kanboard\Event\TaskEvent; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberInterface @@ -11,8 +11,8 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI public static function getSubscribedEvents() { return array( - Task::EVENT_MOVE_COLUMN => 'onMove', - Task::EVENT_CLOSE => 'onClose', + TaskModel::EVENT_MOVE_COLUMN => 'onMove', + TaskModel::EVENT_CLOSE => 'onClose', ); } @@ -20,11 +20,11 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI { $this->logger->debug('Subscriber executed: '.__METHOD__); - if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) { - if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->column->getFirstColumnId($event['project_id']) == $event['src_column_id']) { - $this->taskDuplication->duplicateRecurringTask($event['task_id']); - } elseif ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->column->getLastColumnId($event['project_id']) == $event['dst_column_id']) { - $this->taskDuplication->duplicateRecurringTask($event['task_id']); + if ($event['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) { + if ($event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_FIRST_COLUMN && $this->columnModel->getFirstColumnId($event['project_id']) == $event['src_column_id']) { + $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); + } elseif ($event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_LAST_COLUMN && $this->columnModel->getLastColumnId($event['project_id']) == $event['dst_column_id']) { + $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); } } } @@ -33,8 +33,8 @@ class RecurringTaskSubscriber extends BaseSubscriber implements EventSubscriberI { $this->logger->debug('Subscriber executed: '.__METHOD__); - if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == Task::RECURRING_TRIGGER_CLOSE) { - $this->taskDuplication->duplicateRecurringTask($event['task_id']); + if ($event['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == TaskModel::RECURRING_TRIGGER_CLOSE) { + $this->taskRecurrenceModel->duplicateRecurringTask($event['task_id']); } } } diff --git a/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php b/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php index c0852bc..7e39c12 100644 --- a/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php +++ b/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php @@ -3,7 +3,7 @@ namespace Kanboard\Subscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Kanboard\Model\Subtask; +use Kanboard\Model\SubtaskModel; use Kanboard\Event\SubtaskEvent; class SubtaskTimeTrackingSubscriber extends BaseSubscriber implements EventSubscriberInterface @@ -11,9 +11,9 @@ class SubtaskTimeTrackingSubscriber extends BaseSubscriber implements EventSubsc public static function getSubscribedEvents() { return array( - Subtask::EVENT_CREATE => 'updateTaskTime', - Subtask::EVENT_DELETE => 'updateTaskTime', - Subtask::EVENT_UPDATE => array( + SubtaskModel::EVENT_CREATE => 'updateTaskTime', + SubtaskModel::EVENT_DELETE => 'updateTaskTime', + SubtaskModel::EVENT_UPDATE => array( array('logStartEnd', 10), array('updateTaskTime', 0), ) @@ -24,24 +24,24 @@ class SubtaskTimeTrackingSubscriber extends BaseSubscriber implements EventSubsc { if (isset($event['task_id'])) { $this->logger->debug('Subscriber executed: '.__METHOD__); - $this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']); + $this->subtaskTimeTrackingModel->updateTaskTimeTracking($event['task_id']); } } public function logStartEnd(SubtaskEvent $event) { - if (isset($event['status']) && $this->config->get('subtask_time_tracking') == 1) { + if (isset($event['status']) && $this->configModel->get('subtask_time_tracking') == 1) { $this->logger->debug('Subscriber executed: '.__METHOD__); - $subtask = $this->subtask->getById($event['id']); + $subtask = $this->subtaskModel->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']); + if ($subtask['status'] == SubtaskModel::STATUS_INPROGRESS) { + return $this->subtaskTimeTrackingModel->logStartTime($subtask['id'], $subtask['user_id']); } else { - return $this->subtaskTimeTracking->logEndTime($subtask['id'], $subtask['user_id']); + return $this->subtaskTimeTrackingModel->logEndTime($subtask['id'], $subtask['user_id']); } } } diff --git a/sources/app/Subscriber/TransitionSubscriber.php b/sources/app/Subscriber/TransitionSubscriber.php index bd53748..26d08f8 100644 --- a/sources/app/Subscriber/TransitionSubscriber.php +++ b/sources/app/Subscriber/TransitionSubscriber.php @@ -3,7 +3,7 @@ namespace Kanboard\Subscriber; use Kanboard\Event\TaskEvent; -use Kanboard\Model\Task; +use Kanboard\Model\TaskModel; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class TransitionSubscriber extends BaseSubscriber implements EventSubscriberInterface @@ -11,7 +11,7 @@ class TransitionSubscriber extends BaseSubscriber implements EventSubscriberInte public static function getSubscribedEvents() { return array( - Task::EVENT_MOVE_COLUMN => 'execute', + TaskModel::EVENT_MOVE_COLUMN => 'execute', ); } @@ -22,7 +22,7 @@ class TransitionSubscriber extends BaseSubscriber implements EventSubscriberInte $user_id = $this->userSession->getId(); if (! empty($user_id)) { - $this->transition->save($user_id, $event->getAll()); + $this->transitionModel->save($user_id, $event->getAll()); } } } diff --git a/sources/app/Template/action/index.php b/sources/app/Template/action/index.php index 63d6388..0a94e4f 100644 --- a/sources/app/Template/action/index.php +++ b/sources/app/Template/action/index.php @@ -3,11 +3,11 @@ @@ -63,9 +63,9 @@ - url->link(t('Remove'), 'action', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id']), false, 'popover') ?> + url->link(t('Remove'), 'ActionController', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id']), false, 'popover') ?> - \ No newline at end of file + diff --git a/sources/app/Template/action/remove.php b/sources/app/Template/action/remove.php index 070a791..384bec7 100644 --- a/sources/app/Template/action/remove.php +++ b/sources/app/Template/action/remove.php @@ -8,8 +8,8 @@

- url->link(t('Yes'), 'action', 'remove', array('project_id' => $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'ActionController', 'remove', array('project_id' => $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'ActionController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
- \ No newline at end of file + diff --git a/sources/app/Template/action_creation/create.php b/sources/app/Template/action_creation/create.php index bccb19b..c0d2880 100644 --- a/sources/app/Template/action_creation/create.php +++ b/sources/app/Template/action_creation/create.php @@ -1,7 +1,7 @@ -
+ form->csrf() ?> form->hidden('project_id', $values) ?> @@ -11,6 +11,6 @@
- url->link(t('cancel'), 'Action', 'index', array(), false, 'close-popover') ?> + url->link(t('cancel'), 'ActionController', 'index', array(), false, 'close-popover') ?>
-
\ No newline at end of file + diff --git a/sources/app/Template/action_creation/event.php b/sources/app/Template/action_creation/event.php index e7e5aaf..cdf0031 100644 --- a/sources/app/Template/action_creation/event.php +++ b/sources/app/Template/action_creation/event.php @@ -2,7 +2,7 @@

-
+ form->csrf() ?> @@ -22,6 +22,6 @@
- url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'ActionController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
-
\ No newline at end of file + diff --git a/sources/app/Template/action_creation/params.php b/sources/app/Template/action_creation/params.php index 59ff6ce..fa89217 100644 --- a/sources/app/Template/action_creation/params.php +++ b/sources/app/Template/action_creation/params.php @@ -2,7 +2,7 @@

-
+ form->csrf() ?> @@ -35,6 +35,9 @@ text->contains($param_name, 'link_id')): ?> form->label($param_desc, $param_name) ?> form->select('params['.$param_name.']', $links_list, $values) ?> + + form->label($param_desc, $param_name) ?> + form->select('params['.$param_name.']', $priorities_list, $values) ?> text->contains($param_name, 'duration')): ?> form->label($param_desc, $param_name) ?> form->number('params['.$param_name.']', $values) ?> @@ -47,6 +50,6 @@
- url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'ActionController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
-
\ No newline at end of file + diff --git a/sources/app/Template/activity/filter_dropdown.php b/sources/app/Template/activity/filter_dropdown.php new file mode 100644 index 0000000..8d7a7de --- /dev/null +++ b/sources/app/Template/activity/filter_dropdown.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/sources/app/Template/activity/project.php b/sources/app/Template/activity/project.php index 176d9b9..ce1c8c0 100644 --- a/sources/app/Template/activity/project.php +++ b/sources/app/Template/activity/project.php @@ -1,14 +1,14 @@
- projectHeader->render($project, 'Analytic', $this->app->getRouterAction()) ?> + projectHeader->render($project, 'AnalyticController', $this->app->getRouterAction()) ?> render('event/events', array('events' => $events)) ?> -
\ No newline at end of file + diff --git a/sources/app/Template/analytic/burndown.php b/sources/app/Template/analytic/burndown.php index ed6c8ae..e979595 100644 --- a/sources/app/Template/analytic/burndown.php +++ b/sources/app/Template/analytic/burndown.php @@ -12,7 +12,7 @@
-
+ form->csrf() ?> diff --git a/sources/app/Template/analytic/cfd.php b/sources/app/Template/analytic/cfd.php index ee259c7..8dfb5b0 100644 --- a/sources/app/Template/analytic/cfd.php +++ b/sources/app/Template/analytic/cfd.php @@ -12,7 +12,7 @@
- + form->csrf() ?> diff --git a/sources/app/Template/analytic/compare_hours.php b/sources/app/Template/analytic/compare_hours.php index 8249e7b..70d8d02 100644 --- a/sources/app/Template/analytic/compare_hours.php +++ b/sources/app/Template/analytic/compare_hours.php @@ -34,13 +34,13 @@ getCollection() as $task): ?> - url->link('#'.$this->text->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + url->link('#'.$this->text->e($task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> - url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> - + diff --git a/sources/app/Template/analytic/layout.php b/sources/app/Template/analytic/layout.php index 35793cb..e3c6099 100644 --- a/sources/app/Template/analytic/layout.php +++ b/sources/app/Template/analytic/layout.php @@ -1,5 +1,5 @@
- projectHeader->render($project, 'Listing', 'show') ?> + projectHeader->render($project, 'TaskListController', 'show') ?>
- -asset->js('assets/js/vendor/d3.v3.min.js') ?> -asset->js('assets/js/vendor/c3.min.js') ?> \ No newline at end of file diff --git a/sources/app/Template/analytic/lead_cycle_time.php b/sources/app/Template/analytic/lead_cycle_time.php index 82ffe53..2dccc13 100644 --- a/sources/app/Template/analytic/lead_cycle_time.php +++ b/sources/app/Template/analytic/lead_cycle_time.php @@ -16,7 +16,7 @@
- + form->csrf() ?> diff --git a/sources/app/Template/analytic/sidebar.php b/sources/app/Template/analytic/sidebar.php index 76289b9..de3dccf 100644 --- a/sources/app/Template/analytic/sidebar.php +++ b/sources/app/Template/analytic/sidebar.php @@ -1,29 +1,29 @@ diff --git a/sources/app/Template/app/calendar.php b/sources/app/Template/app/calendar.php deleted file mode 100644 index a154203..0000000 --- a/sources/app/Template/app/calendar.php +++ /dev/null @@ -1,5 +0,0 @@ -
-
diff --git a/sources/app/Template/app/sidebar.php b/sources/app/Template/app/sidebar.php deleted file mode 100644 index 66d15b1..0000000 --- a/sources/app/Template/app/sidebar.php +++ /dev/null @@ -1,27 +0,0 @@ - \ No newline at end of file diff --git a/sources/app/Template/auth/index.php b/sources/app/Template/auth/index.php index cc56217..45bbdb8 100644 --- a/sources/app/Template/auth/index.php +++ b/sources/app/Template/auth/index.php @@ -7,7 +7,7 @@ - + form->csrf() ?> @@ -19,7 +19,7 @@ form->label(t('Enter the text below'), 'captcha') ?> - + Captcha form->text('captcha', array(), $errors, array('required')) ?> @@ -32,11 +32,11 @@ app->config('password_reset') == 1): ?>
- url->link(t('Forgot password?'), 'PasswordReset', 'create') ?> + url->link(t('Forgot password?'), 'PasswordResetController', 'create') ?>
hook->render('template:auth:login-form:after') ?> - \ No newline at end of file + diff --git a/sources/app/Template/avatar_file/show.php b/sources/app/Template/avatar_file/show.php index 266a2cc..37c56ce 100644 --- a/sources/app/Template/avatar_file/show.php +++ b/sources/app/Template/avatar_file/show.php @@ -4,17 +4,20 @@ avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], '') ?> -
+
+ + url->link(t('Remove my image'), 'AvatarFileController', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?> + +
+ +
+ +

+ form->csrf() ?> - form->label(t('Upload my avatar image'), 'avatar') ?> form->file('avatar') ?>
- - url->link(t('Remove my image'), 'AvatarFile', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?> - - - - url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> +
diff --git a/sources/app/Template/board/popover_assignee.php b/sources/app/Template/board/popover_assignee.php deleted file mode 100644 index 87e1681..0000000 --- a/sources/app/Template/board/popover_assignee.php +++ /dev/null @@ -1,20 +0,0 @@ -
- -
- - form->csrf() ?> - - form->hidden('id', $values) ?> - form->hidden('project_id', $values) ?> - - task->selectAssignee($users_list, $values, array(), array('autofocus')) ?> - -
- - - url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> -
-
-
\ No newline at end of file diff --git a/sources/app/Template/board/popover_category.php b/sources/app/Template/board/popover_category.php deleted file mode 100644 index e379476..0000000 --- a/sources/app/Template/board/popover_category.php +++ /dev/null @@ -1,20 +0,0 @@ -
- -
- - form->csrf() ?> - - form->hidden('id', $values) ?> - form->hidden('project_id', $values) ?> - - task->selectCategory($categories_list, $values, array(), array('autofocus'), true) ?> - -
- - - url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> -
-
-
\ No newline at end of file diff --git a/sources/app/Template/board/table_column.php b/sources/app/Template/board/table_column.php index 48538c8..6336234 100644 --- a/sources/app/Template/board/table_column.php +++ b/sources/app/Template/board/table_column.php @@ -12,15 +12,15 @@
- user->hasProjectAccess('taskcreation', 'create', $column['project_id'])): ?> + user->hasProjectAccess('TaskCreationController', 'show', $column['project_id'])): ?>
- url->link('+', 'taskcreation', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?> + url->link('+', 'TaskCreationController', 'show', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?>
- 1 && ! empty($column['nb_column_tasks'])): ?> + 1 && ! empty($column['column_nb_tasks'])): ?> - () + () @@ -35,11 +35,17 @@ - user->hasProjectAccess('BoardPopover', 'closeColumnTasks', $column['project_id']) && $column['nb_tasks'] > 0): ?> + user->hasProjectAccess('TaskCreationController', 'show', $column['project_id'])): ?>
  • - - url->link(t('Close all tasks of this column'), 'BoardPopover', 'confirmCloseColumnTasks', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?> + + url->link(t('Create tasks in bulk'), 'TaskBulkController', 'show', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?>
  • + 0): ?> +
  • + + url->link(t('Close all tasks of this column'), 'BoardPopoverController', 'confirmCloseColumnTasks', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?> +
  • + @@ -47,7 +53,7 @@ - '> +   diff --git a/sources/app/Template/board/table_container.php b/sources/app/Template/board/table_container.php index e30f9ce..a93e700 100644 --- a/sources/app/Template/board/table_container.php +++ b/sources/app/Template/board/table_container.php @@ -10,10 +10,10 @@ class="board-project-" data-project-id="" data-check-interval="" - data-save-url="url->href('board', 'save', array('project_id' => $project['id'])) ?>" - data-reload-url="url->href('board', 'reload', array('project_id' => $project['id'])) ?>" - data-check-url="url->href('board', 'check', array('project_id' => $project['id'], 'timestamp' => time())) ?>" - data-task-creation-url="url->href('taskcreation', 'create', array('project_id' => $project['id'])) ?>" + data-save-url="url->href('BoardAjaxController', 'save', array('project_id' => $project['id'])) ?>" + data-reload-url="url->href('BoardAjaxController', 'reload', array('project_id' => $project['id'])) ?>" + data-check-url="url->href('BoardAjaxController', 'check', array('project_id' => $project['id'], 'timestamp' => time())) ?>" + data-task-creation-url="url->href('TaskCreationController', 'show', array('project_id' => $project['id'])) ?>" > @@ -55,4 +55,4 @@ -
    \ No newline at end of file + diff --git a/sources/app/Template/board/table_swimlane.php b/sources/app/Template/board/table_swimlane.php index 349b9ac..c5937e0 100644 --- a/sources/app/Template/board/table_swimlane.php +++ b/sources/app/Template/board/table_swimlane.php @@ -14,7 +14,7 @@ + data-href="url->href('BoardTooltipController', 'swimlane', array('swimlane_id' => $swimlane['id'], 'project_id' => $project['id'])) ?>"> diff --git a/sources/app/Template/board/task_avatar.php b/sources/app/Template/board/task_avatar.php index 39f6b54..28e0813 100644 --- a/sources/app/Template/board/task_avatar.php +++ b/sources/app/Template/board/task_avatar.php @@ -1,9 +1,9 @@
    user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> + user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])): ?> class="task-board-assignee task-board-change-assignee" - data-url="url->href('BoardPopover', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"> + data-url="url->href('TaskModificationController', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"> class="task-board-assignee"> diff --git a/sources/app/Template/board/task_footer.php b/sources/app/Template/board/task_footer.php index a9d381a..bc34363 100644 --- a/sources/app/Template/board/task_footer.php +++ b/sources/app/Template/board/task_footer.php @@ -6,18 +6,28 @@ url->link( $this->text->e($task['category_name']), - 'boardPopover', - 'changeCategory', + 'TaskModificationController', + 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover' . (! empty($task['category_description']) ? ' tooltip' : ''), - ! empty($task['category_description']) ? $this->text->markdown($task['category_description']) : t('Change category') + ! empty($task['category_description']) ? $this->text->markdownAttribute($task['category_description']) : t('Change category') ) ?>
    + +
    +
      + +
    • text->e($tag['name']) ?>
    • + +
    +
    + +
    @@ -27,42 +37,46 @@ - + + + $task['date_due']): ?> + + dt->date($task['date_due']) ?> - - + + - - + + - + - + -   +   -   +   -   +   - + @@ -76,7 +90,7 @@ - + hook->render('template:board:task:icons', array('task' => $task)) ?> task->formatPriority($project, $task) ?> diff --git a/sources/app/Template/board/task_menu.php b/sources/app/Template/board/task_menu.php deleted file mode 100644 index c0d97cd..0000000 --- a/sources/app/Template/board/task_menu.php +++ /dev/null @@ -1,18 +0,0 @@ - - -
      -
    •  url->link(t('Change assignee'), 'BoardPopover', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • -
    •  url->link(t('Change category'), 'BoardPopover', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • -
    •  url->link(t('Change description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • -
    •  url->link(t('Edit this task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • -
    •  url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • -
    •  url->link(t('Add internal link'), 'TaskInternalLink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • -
    •  url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • -
    •  url->link(t('Add a screenshot'), 'BoardPopover', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • - -
    •  url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • - -
    •  url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
    • - -
    -
    \ No newline at end of file diff --git a/sources/app/Template/board/task_private.php b/sources/app/Template/board/task_private.php index 19bcbcf..94b396a 100644 --- a/sources/app/Template/board/task_private.php +++ b/sources/app/Template/board/task_private.php @@ -1,6 +1,6 @@
    + data-task-url="url->href('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"> board->isCollapsed($task['project_id'])): ?>
    - user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> - render('board/task_menu', array('task' => $task)) ?> + user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])): ?> + render('task/dropdown', array('task' => $task)) ?> @@ -27,13 +27,13 @@ text->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?> - - url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title tooltip', $this->text->e($task['title'])) ?> + url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title tooltip', $this->text->e($task['title'])) ?>
    - user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> - render('board/task_menu', array('task' => $task)) ?> + user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])): ?> + render('task/dropdown', array('task' => $task)) ?> @@ -48,7 +48,7 @@ hook->render('template:board:private:task:before-title', array('task' => $task)) ?>
    - url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?>
    hook->render('template:board:private:task:after-title', array('task' => $task)) ?> diff --git a/sources/app/Template/board/task_public.php b/sources/app/Template/board/task_public.php index a53d42c..82eb653 100644 --- a/sources/app/Template/board/task_public.php +++ b/sources/app/Template/board/task_public.php @@ -1,6 +1,6 @@
    - url->link('#'.$task['id'], 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> + url->link('#'.$task['id'], 'TaskViewController', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> @@ -12,7 +12,7 @@ hook->render('template:board:public:task:before-title', array('task' => $task)) ?>
    - url->link($this->text->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> + url->link($this->text->e($task['title']), 'TaskViewController', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?>
    hook->render('template:board:public:task:after-title', array('task' => $task)) ?> @@ -21,4 +21,4 @@ 'not_editable' => $not_editable, 'project' => $project, )) ?> -
    \ No newline at end of file +
    diff --git a/sources/app/Template/board/tooltip_files.php b/sources/app/Template/board/tooltip_files.php index 5ade5b5..6f9e264 100644 --- a/sources/app/Template/board/tooltip_files.php +++ b/sources/app/Template/board/tooltip_files.php @@ -9,9 +9,9 @@ - url->link(t('download'), 'FileViewer', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + url->link(t('download'), 'FileViewerController', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> -   url->link(t('open file'), 'FileViewer', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> +   url->link(t('open file'), 'FileViewerController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> diff --git a/sources/app/Template/board/tooltip_tasklinks.php b/sources/app/Template/board/tooltip_tasklinks.php index 6424c39..d1156cb 100644 --- a/sources/app/Template/board/tooltip_tasklinks.php +++ b/sources/app/Template/board/tooltip_tasklinks.php @@ -12,7 +12,7 @@ url->link( $this->text->e('#'.$link['task_id'].' '.$link['title']), - 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), + 'TaskViewController', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), false, $link['is_active'] ? '' : 'task-link-closed' ) ?> @@ -31,4 +31,4 @@ -
    \ No newline at end of file +
    diff --git a/sources/app/Template/board/view_private.php b/sources/app/Template/board/view_private.php index 1370227..a89e7d2 100644 --- a/sources/app/Template/board/view_private.php +++ b/sources/app/Template/board/view_private.php @@ -1,6 +1,6 @@
    - projectHeader->render($project, 'Board', 'show', true) ?> + projectHeader->render($project, 'BoardViewController', 'show', true) ?> render('board/table_container', array( 'project' => $project, diff --git a/sources/app/Template/board/popover_close_all_tasks_column.php b/sources/app/Template/board_popover/close_all_tasks_column.php similarity index 74% rename from sources/app/Template/board/popover_close_all_tasks_column.php rename to sources/app/Template/board_popover/close_all_tasks_column.php index 5090f49..57f703e 100644 --- a/sources/app/Template/board/popover_close_all_tasks_column.php +++ b/sources/app/Template/board_popover/close_all_tasks_column.php @@ -2,7 +2,7 @@ -
    + form->csrf() ?> form->hidden('column_id', $values) ?> form->hidden('swimlane_id', $values) ?> @@ -12,7 +12,7 @@
    - url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'BoardViewController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/calendar/show.php b/sources/app/Template/calendar/show.php index f00e810..3635f62 100644 --- a/sources/app/Template/calendar/show.php +++ b/sources/app/Template/calendar/show.php @@ -1,9 +1,9 @@
    - projectHeader->render($project, 'Calendar', 'show') ?> + projectHeader->render($project, 'CalendarController', 'show') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/category/edit.php b/sources/app/Template/category/edit.php index 7b59268..fac56db 100644 --- a/sources/app/Template/category/edit.php +++ b/sources/app/Template/category/edit.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> @@ -18,6 +18,6 @@
    - url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'CategoryController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/category/index.php b/sources/app/Template/category/index.php index b3bdfd8..a103d89 100644 --- a/sources/app/Template/category/index.php +++ b/sources/app/Template/category/index.php @@ -15,10 +15,10 @@ @@ -31,7 +31,7 @@ -
    + form->csrf() ?> form->hidden('project_id', $values) ?> @@ -42,4 +42,4 @@
    -
    \ No newline at end of file + diff --git a/sources/app/Template/category/remove.php b/sources/app/Template/category/remove.php index cad58d3..e7b9c9b 100644 --- a/sources/app/Template/category/remove.php +++ b/sources/app/Template/category/remove.php @@ -9,9 +9,9 @@

    - url->link(t('Yes'), 'category', 'remove', array('project_id' => $project['id'], 'category_id' => $category['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'CategoryController', 'remove', array('project_id' => $project['id'], 'category_id' => $category['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'CategoryController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/column/create.php b/sources/app/Template/column/create.php index 2d325f7..023de52 100644 --- a/sources/app/Template/column/create.php +++ b/sources/app/Template/column/create.php @@ -1,7 +1,7 @@ -
    + form->csrf() ?> @@ -21,4 +21,4 @@ url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> -
    \ No newline at end of file + diff --git a/sources/app/Template/column/edit.php b/sources/app/Template/column/edit.php index 412858a..a742e4b 100644 --- a/sources/app/Template/column/edit.php +++ b/sources/app/Template/column/edit.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> @@ -21,6 +21,6 @@
    - url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'ColumnController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/column/index.php b/sources/app/Template/column/index.php index eef176f..04760a1 100644 --- a/sources/app/Template/column/index.php +++ b/sources/app/Template/column/index.php @@ -3,17 +3,17 @@ -

    +

    + data-save-position-url="url->href('ColumnController', 'move', array('project_id' => $project['id'])) ?>"> @@ -28,7 +28,7 @@ text->e($column['title']) ?> - '> + @@ -41,10 +41,10 @@
    • - url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?> + url->link(t('Edit'), 'ColumnController', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?>
    • - url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?> + url->link(t('Remove'), 'ColumnController', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id']), false, 'popover') ?>
    diff --git a/sources/app/Template/column/remove.php b/sources/app/Template/column/remove.php index ccab889..b231a9a 100644 --- a/sources/app/Template/column/remove.php +++ b/sources/app/Template/column/remove.php @@ -9,7 +9,7 @@

    - url->link(t('Yes'), 'column', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> - url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('Yes'), 'ColumnController', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> + url->link(t('cancel'), 'ColumnController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/comment/create.php b/sources/app/Template/comment/create.php index b6c27ae..0358107 100644 --- a/sources/app/Template/comment/create.php +++ b/sources/app/Template/comment/create.php @@ -1,7 +1,7 @@ -
    + form->csrf() ?> form->hidden('task_id', $values) ?> form->hidden('user_id', $values) ?> @@ -15,7 +15,7 @@ 'autofocus', 'required', 'placeholder="'.t('Leave a comment').'"', - 'data-mention-search-url="'.$this->url->href('UserHelper', 'mention', array('project_id' => $task['project_id'])).'"', + 'data-mention-search-url="'.$this->url->href('UserAjaxController', 'mention', array('project_id' => $task['project_id'])).'"', ), 'markdown-editor' ) ?> @@ -24,6 +24,6 @@
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/comment/edit.php b/sources/app/Template/comment/edit.php index 4036b67..f69fc0c 100644 --- a/sources/app/Template/comment/edit.php +++ b/sources/app/Template/comment/edit.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> form->hidden('id', $values) ?> @@ -22,6 +22,6 @@
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/comment/remove.php b/sources/app/Template/comment/remove.php index 3174df0..55587b6 100644 --- a/sources/app/Template/comment/remove.php +++ b/sources/app/Template/comment/remove.php @@ -14,8 +14,8 @@ )) ?>
    - url->link(t('Yes'), 'comment', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'CommentController', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/comment/show.php b/sources/app/Template/comment/show.php index 3f45e2e..8419a14 100644 --- a/sources/app/Template/comment/show.php +++ b/sources/app/Template/comment/show.php @@ -12,29 +12,7 @@
    - - text->markdown( - $comment['comment'], - array( - 'controller' => 'task', - 'action' => 'readonly', - 'params' => array( - 'token' => $project['token'] - ) - ) - ) ?> - - text->markdown( - $comment['comment'], - array( - 'controller' => 'task', - 'action' => 'show', - 'params' => array( - 'project_id' => $task['project_id'] - ) - ) - ) ?> - + text->markdown($comment['comment'], isset($is_public) && $is_public) ?>
    @@ -48,11 +26,11 @@ user->isAdmin() || $this->user->isCurrentUser($comment['user_id']))): ?>
  • - url->link(t('remove'), 'comment', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), false, 'popover') ?> + url->link(t('remove'), 'CommentController', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), false, 'popover') ?>
  • - url->link(t('edit'), 'comment', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), false, 'popover') ?> + url->link(t('edit'), 'CommentController', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), false, 'popover') ?>
  • diff --git a/sources/app/Template/comments/create.php b/sources/app/Template/comments/create.php index a638d3b..3fa6ddc 100644 --- a/sources/app/Template/comments/create.php +++ b/sources/app/Template/comments/create.php @@ -1,4 +1,4 @@ -
    + form->csrf() ?> form->hidden('task_id', $values) ?> form->hidden('user_id', $values) ?> @@ -12,7 +12,7 @@ 'data-markdown-editor-disable-toolbar="true"', 'required', 'placeholder="'.t('Leave a comment').'"', - 'data-mention-search-url="'.$this->url->href('UserHelper', 'mention', array('project_id' => $task['project_id'])).'"', + 'data-mention-search-url="'.$this->url->href('UserAjaxController', 'mention', array('project_id' => $task['project_id'])).'"', ), 'markdown-editor' ) ?> diff --git a/sources/app/Template/comments/show.php b/sources/app/Template/comments/show.php index 76af559..43f6b2c 100644 --- a/sources/app/Template/comments/show.php +++ b/sources/app/Template/comments/show.php @@ -6,7 +6,7 @@
    - url->link(t('change sorting'), 'comment', 'toggleSorting', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + url->link(t('change sorting'), 'CommentController', 'toggleSorting', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
    @@ -30,4 +30,4 @@ )) ?> - \ No newline at end of file + diff --git a/sources/app/Template/config/about.php b/sources/app/Template/config/about.php index 5c1f4b3..8e2d132 100644 --- a/sources/app/Template/config/about.php +++ b/sources/app/Template/config/about.php @@ -5,7 +5,7 @@
    • - http://kanboard.net/ + https://kanboard.net/
    • @@ -65,11 +65,11 @@ text->bytes($db_size) ?>
    • - url->link(t('Download the database'), 'config', 'downloadDb', array(), true) ?>  + url->link(t('Download the database'), 'ConfigController', 'downloadDb', array(), true) ?> 
    • - url->link(t('Optimize the database'), 'config', 'optimizeDb', array(), true) ?>  + url->link(t('Optimize the database'), 'ConfigController', 'optimizeDb', array(), true) ?> 
    @@ -82,5 +82,5 @@

    - -
    \ No newline at end of file + + diff --git a/sources/app/Template/config/api.php b/sources/app/Template/config/api.php index 3ebbb95..95f7735 100644 --- a/sources/app/Template/config/api.php +++ b/sources/app/Template/config/api.php @@ -12,7 +12,7 @@
  • - url->link(t('Reset token'), 'config', 'token', array('type' => 'api'), true) ?> + url->link(t('Reset token'), 'ConfigController', 'token', array('type' => 'api'), true) ?>
  • - \ No newline at end of file + diff --git a/sources/app/Template/config/application.php b/sources/app/Template/config/application.php index 259756b..0f842f6 100644 --- a/sources/app/Template/config/application.php +++ b/sources/app/Template/config/application.php @@ -1,7 +1,7 @@ - + form->csrf() ?> diff --git a/sources/app/Template/config/board.php b/sources/app/Template/config/board.php index ba1bab5..62a736e 100644 --- a/sources/app/Template/config/board.php +++ b/sources/app/Template/config/board.php @@ -1,7 +1,7 @@ - + form->csrf() ?> diff --git a/sources/app/Template/config/calendar.php b/sources/app/Template/config/calendar.php index b7b230d..90e034e 100644 --- a/sources/app/Template/config/calendar.php +++ b/sources/app/Template/config/calendar.php @@ -2,7 +2,7 @@

    - + form->csrf() ?> @@ -31,4 +31,4 @@ -
    \ No newline at end of file + diff --git a/sources/app/Template/config/email.php b/sources/app/Template/config/email.php new file mode 100644 index 0000000..6ff76ec --- /dev/null +++ b/sources/app/Template/config/email.php @@ -0,0 +1,18 @@ + +
    + form->csrf() ?> + + form->label(t('Email sender address'), 'mail_sender_address') ?> + form->text('mail_sender_address', $values, $errors, array('placeholder="'.MAIL_FROM.'"')) ?> + + form->label(t('Email transport'), 'mail_transport') ?> + form->select('mail_transport', $mail_transports, $values, $errors) ?> + + hook->render('template:config:email', array('values' => $values, 'errors' => $errors)) ?> + +
    + +
    + diff --git a/sources/app/Template/config/integrations.php b/sources/app/Template/config/integrations.php index e404c52..3ba4e86 100644 --- a/sources/app/Template/config/integrations.php +++ b/sources/app/Template/config/integrations.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> hook->render('template:config:integrations', array('values' => $values)) ?> @@ -14,4 +14,4 @@
    - \ No newline at end of file + diff --git a/sources/app/Template/config/keyboard_shortcuts.php b/sources/app/Template/config/keyboard_shortcuts.php index da53266..1b1a947 100644 --- a/sources/app/Template/config/keyboard_shortcuts.php +++ b/sources/app/Template/config/keyboard_shortcuts.php @@ -19,7 +19,6 @@

    • = e
    • -
    • = d
    • = s
    • = c
    • = l
    • @@ -33,4 +32,4 @@
    • = ESC
    • = CTRL+ENTER ⌘+ENTER
    - \ No newline at end of file + diff --git a/sources/app/Template/config/layout.php b/sources/app/Template/config/layout.php index f34caaa..6eafa59 100644 --- a/sources/app/Template/config/layout.php +++ b/sources/app/Template/config/layout.php @@ -1,10 +1,9 @@
    -
    \ No newline at end of file + diff --git a/sources/app/Template/config/plugins.php b/sources/app/Template/config/plugins.php deleted file mode 100644 index 04b3f09..0000000 --- a/sources/app/Template/config/plugins.php +++ /dev/null @@ -1,30 +0,0 @@ - - - -

    - -
    - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sources/app/Template/config/project.php b/sources/app/Template/config/project.php index b6b7ec2..6d8d131 100644 --- a/sources/app/Template/config/project.php +++ b/sources/app/Template/config/project.php @@ -1,7 +1,7 @@ - +form->csrf() ?> diff --git a/sources/app/Template/config/sidebar.php b/sources/app/Template/config/sidebar.php index dd51bc7..e304f0d 100644 --- a/sources/app/Template/config/sidebar.php +++ b/sources/app/Template/config/sidebar.php @@ -1,39 +1,42 @@ \ No newline at end of file + diff --git a/sources/app/Template/config/webhook.php b/sources/app/Template/config/webhook.php index b96979a..e324587 100644 --- a/sources/app/Template/config/webhook.php +++ b/sources/app/Template/config/webhook.php @@ -2,7 +2,7 @@

    - + form->csrf() ?> @@ -16,7 +16,7 @@
      @@ -25,11 +25,7 @@ text->e($values['webhook_token']) ?>
    • - - -
    • -
    • - url->link(t('Reset token'), 'config', 'token', array('type' => 'webhook'), true) ?> + url->link(t('Reset token'), 'ConfigController', 'token', array('type' => 'webhook'), true) ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/currency/index.php b/sources/app/Template/currency/index.php index d35ac45..9881cee 100644 --- a/sources/app/Template/currency/index.php +++ b/sources/app/Template/currency/index.php @@ -24,7 +24,7 @@

    - +form->csrf() ?> @@ -38,7 +38,7 @@

    - +form->csrf() ?> diff --git a/sources/app/Template/custom_filter/add.php b/sources/app/Template/custom_filter/add.php index e3e144a..3801cc3 100644 --- a/sources/app/Template/custom_filter/add.php +++ b/sources/app/Template/custom_filter/add.php @@ -1,7 +1,7 @@ - +form->csrf() ?> form->hidden('project_id', $values) ?> @@ -12,7 +12,7 @@ form->label(t('Filter'), 'filter') ?> form->text('filter', $values, $errors, array('required', 'maxlength="100"')) ?> - user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?> + user->hasProjectAccess('ProjectEditController', 'edit', $project['id'])): ?> form->checkbox('is_shared', t('Share with all project members'), 1) ?> @@ -21,4 +21,4 @@
    - \ No newline at end of file + diff --git a/sources/app/Template/custom_filter/edit.php b/sources/app/Template/custom_filter/edit.php index 5d07e8c..26da8da 100644 --- a/sources/app/Template/custom_filter/edit.php +++ b/sources/app/Template/custom_filter/edit.php @@ -2,7 +2,7 @@

    - +form->csrf() ?> @@ -16,7 +16,7 @@ form->label(t('Filter'), 'filter') ?> form->text('filter', $values, $errors, array('required', 'maxlength="100"')) ?> - user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?> + user->hasProjectAccess('ProjectEditController', 'edit', $project['id'])): ?> form->checkbox('is_shared', t('Share with all project members'), 1, $values['is_shared'] == 1) ?> form->hidden('is_shared', $values) ?> @@ -27,6 +27,6 @@
    - url->link(t('cancel'), 'customfilter', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'CustomFilterController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/custom_filter/index.php b/sources/app/Template/custom_filter/index.php index 12a4eec..08c8040 100644 --- a/sources/app/Template/custom_filter/index.php +++ b/sources/app/Template/custom_filter/index.php @@ -32,12 +32,12 @@
    - getPluginHomepage()): ?> - text->e($plugin->getPluginName()) ?> - - text->e($plugin->getPluginName()) ?> - - text->e($plugin->getPluginAuthor()) ?>text->e($plugin->getPluginVersion()) ?>text->e($plugin->getPluginDescription()) ?>
    text->e($filter['owner_name'] ?: $filter['owner_username']) ?> - user->getId() || $this->user->hasProjectAccess('customfilter', 'edit', $project['id'])): ?> + user->getId() || $this->user->hasProjectAccess('CustomFilterController', 'edit', $project['id'])): ?> diff --git a/sources/app/Template/custom_filter/remove.php b/sources/app/Template/custom_filter/remove.php index d4c67a2..609f19b 100644 --- a/sources/app/Template/custom_filter/remove.php +++ b/sources/app/Template/custom_filter/remove.php @@ -9,9 +9,9 @@

    - url->link(t('Yes'), 'customfilter', 'remove', array('project_id' => $project['id'], 'filter_id' => $filter['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'CustomFilterController', 'remove', array('project_id' => $project['id'], 'filter_id' => $filter['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'customfilter', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'CustomFilterController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/app/activity.php b/sources/app/Template/dashboard/activity.php similarity index 100% rename from sources/app/Template/app/activity.php rename to sources/app/Template/dashboard/activity.php diff --git a/sources/app/Template/dashboard/calendar.php b/sources/app/Template/dashboard/calendar.php new file mode 100644 index 0000000..75c96d8 --- /dev/null +++ b/sources/app/Template/dashboard/calendar.php @@ -0,0 +1,5 @@ +
    +
    diff --git a/sources/app/Template/app/layout.php b/sources/app/Template/dashboard/layout.php similarity index 69% rename from sources/app/Template/app/layout.php rename to sources/app/Template/dashboard/layout.php index 2a32ac0..795537a 100644 --- a/sources/app/Template/app/layout.php +++ b/sources/app/Template/dashboard/layout.php @@ -1,25 +1,25 @@
    @@ -29,4 +29,4 @@
    - \ No newline at end of file + diff --git a/sources/app/Template/app/notifications.php b/sources/app/Template/dashboard/notifications.php similarity index 59% rename from sources/app/Template/app/notifications.php rename to sources/app/Template/dashboard/notifications.php index 4cb3c57..3b70b49 100644 --- a/sources/app/Template/app/notifications.php +++ b/sources/app/Template/dashboard/notifications.php @@ -2,13 +2,13 @@

    -

    +

    • - url->link(t('Mark all as read'), 'webNotification', 'flush', array('user_id' => $user['id'])) ?> + url->link(t('Mark all as read'), 'WebNotificationController', 'flush', array('user_id' => $user['id'])) ?>
    @@ -36,16 +36,10 @@ - text->contains($notification['event_name'], 'comment')): ?> - url->link($notification['title'], 'task', 'show', array('task_id' => $notification['event_data']['task']['id'], 'project_id' => $notification['event_data']['task']['project_id']), false, '', '', false, 'comment-'.$notification['event_data']['comment']['id']) ?> - text->contains($notification['event_name'], 'task.overdue')): ?> - 1): ?> - - - url->link($notification['title'], 'task', 'show', array('task_id' => $notification['event_data']['tasks'][0]['id'], 'project_id' => $notification['event_data']['tasks'][0]['project_id'])) ?> - + text->contains($notification['event_name'], 'task.overdue') && count($notification['event_data']['tasks']) > 1): ?> + - url->link($notification['title'], 'task', 'show', array('task_id' => $notification['event_data']['task']['id'], 'project_id' => $notification['event_data']['task']['project_id'])) ?> + url->link($notification['title'], 'WebNotificationController', 'redirect', array('notification_id' => $notification['id'], 'user_id' => $user['id'])) ?>
    @@ -53,9 +47,9 @@ - url->link(t('Mark as read'), 'webNotification', 'remove', array('user_id' => $user['id'], 'notification_id' => $notification['id'])) ?> + url->link(t('Mark as read'), 'WebNotificationController', 'remove', array('user_id' => $user['id'], 'notification_id' => $notification['id'])) ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/app/projects.php b/sources/app/Template/dashboard/projects.php similarity index 61% rename from sources/app/Template/app/projects.php rename to sources/app/Template/dashboard/projects.php index 4ab8b10..962e4d8 100644 --- a/sources/app/Template/app/projects.php +++ b/sources/app/Template/dashboard/projects.php @@ -1,5 +1,5 @@ isEmpty()): ?>

    @@ -8,13 +8,14 @@ order('Id', 'id') ?> order('', 'is_private') ?> - order(t('Project'), \Kanboard\Model\Project::TABLE.'.name') ?> + order(t('Project'), \Kanboard\Model\ProjectModel::TABLE.'.name') ?> + getCollection() as $project): ?> - url->link('#'.$project['id'], 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link') ?> + render('project/dropdown', array('project' => $project)) ?> @@ -22,26 +23,30 @@ - user->hasProjectAccess('gantt', 'project', $project['id'])): ?> - url->link('', 'gantt', 'project', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?> + user->hasProjectAccess('TaskGanttController', 'show', $project['id'])): ?> + url->link('', 'TaskGanttController', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?> - url->link('', 'listing', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('List')) ?>  - url->link('', 'calendar', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Calendar')) ?>  + url->link('', 'TaskListController', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('List')) ?>  + url->link('', 'CalendarController', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Calendar')) ?>  - url->link($this->text->e($project['name']), 'board', 'show', array('project_id' => $project['id'])) ?> + url->link($this->text->e($project['name']), 'BoardViewController', 'show', array('project_id' => $project['id'])) ?> - '> + + + + text->e($column['title']) ?> + diff --git a/sources/app/Template/app/overview.php b/sources/app/Template/dashboard/show.php similarity index 60% rename from sources/app/Template/app/overview.php rename to sources/app/Template/dashboard/show.php index 0b35479..637b60f 100644 --- a/sources/app/Template/app/overview.php +++ b/sources/app/Template/dashboard/show.php @@ -1,12 +1,12 @@
    -render('app/projects', array('paginator' => $project_paginator, 'user' => $user)) ?> -render('app/tasks', array('paginator' => $task_paginator, 'user' => $user)) ?> -render('app/subtasks', array('paginator' => $subtask_paginator, 'user' => $user)) ?> \ No newline at end of file +render('dashboard/projects', array('paginator' => $project_paginator, 'user' => $user)) ?> +render('dashboard/tasks', array('paginator' => $task_paginator, 'user' => $user)) ?> +render('dashboard/subtasks', array('paginator' => $subtask_paginator, 'user' => $user)) ?> diff --git a/sources/app/Template/dashboard/sidebar.php b/sources/app/Template/dashboard/sidebar.php new file mode 100644 index 0000000..86cc20f --- /dev/null +++ b/sources/app/Template/dashboard/sidebar.php @@ -0,0 +1,27 @@ + diff --git a/sources/app/Template/app/subtasks.php b/sources/app/Template/dashboard/subtasks.php similarity index 81% rename from sources/app/Template/app/subtasks.php rename to sources/app/Template/dashboard/subtasks.php index cca0948..8e0aa3c 100644 --- a/sources/app/Template/app/subtasks.php +++ b/sources/app/Template/dashboard/subtasks.php @@ -1,5 +1,5 @@ isEmpty()): ?>

    @@ -18,10 +18,10 @@ render('task/dropdown', array('task' => array('id' => $subtask['task_id'], 'project_id' => $subtask['project_id']))) ?> - url->link($this->text->e($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?> + url->link($this->text->e($subtask['project_name']), 'BoardViewController', 'show', array('project_id' => $subtask['project_id'])) ?> - url->link($this->text->e($subtask['task_name']), 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> + url->link($this->text->e($subtask['task_name']), 'TaskViewController', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> subtask->toggleStatus($subtask, $subtask['project_id']) ?> @@ -40,4 +40,4 @@ - \ No newline at end of file + diff --git a/sources/app/Template/app/tasks.php b/sources/app/Template/dashboard/tasks.php similarity index 64% rename from sources/app/Template/app/tasks.php rename to sources/app/Template/dashboard/tasks.php index d7826fb..4b83a96 100644 --- a/sources/app/Template/app/tasks.php +++ b/sources/app/Template/dashboard/tasks.php @@ -1,5 +1,5 @@ isEmpty()): ?>

    @@ -9,8 +9,10 @@ order('Id', 'tasks.id') ?> order(t('Project'), 'project_name') ?> order(t('Task'), 'title') ?> + order('Priority', 'tasks.priority') ?> - order(t('Due date'), 'date_due') ?> + order(t('Due date'), 'date_due') ?> + order(t('Column'), 'column_title') ?> getCollection() as $task): ?> @@ -18,10 +20,15 @@ render('task/dropdown', array('task' => $task)) ?> - url->link($this->text->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + url->link($this->text->e($task['project_name']), 'BoardViewController', 'show', array('project_id' => $task['project_id'])) ?> - url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + + + = 0): ?> + Ptext->e($task['priority'])?> + @@ -35,9 +42,12 @@ dt->date($task['date_due']) ?> + + text->e($task['column_title']) ?> + - \ No newline at end of file + diff --git a/sources/app/Template/doc/show.php b/sources/app/Template/doc/show.php index 8fbadc9..a8dbd76 100644 --- a/sources/app/Template/doc/show.php +++ b/sources/app/Template/doc/show.php @@ -3,11 +3,11 @@
    • - url->link(t('Table of contents'), 'doc', 'show', array('file' => 'index')) ?> + url->link(t('Table of contents'), 'DocumentationController', 'show', array('file' => 'index')) ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/event/comment_create.php b/sources/app/Template/event/comment_create.php index 9869c94..45132e6 100644 --- a/sources/app/Template/event/comment_create.php +++ b/sources/app/Template/event/comment_create.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    diff --git a/sources/app/Template/event/comment_update.php b/sources/app/Template/event/comment_update.php index 19420cf..5a0821b 100644 --- a/sources/app/Template/event/comment_update.php +++ b/sources/app/Template/event/comment_update.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    diff --git a/sources/app/Template/event/subtask_create.php b/sources/app/Template/event/subtask_create.php index 7e41d5c..1bf36c0 100644 --- a/sources/app/Template/event/subtask_create.php +++ b/sources/app/Template/event/subtask_create.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/sources/app/Template/event/subtask_update.php b/sources/app/Template/event/subtask_update.php index 9fea4fe..201402f 100644 --- a/sources/app/Template/event/subtask_update.php +++ b/sources/app/Template/event/subtask_update.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    diff --git a/sources/app/Template/event/task_assignee_change.php b/sources/app/Template/event/task_assignee_change.php index db78e7d..7c96222 100644 --- a/sources/app/Template/event/task_assignee_change.php +++ b/sources/app/Template/event/task_assignee_change.php @@ -4,14 +4,14 @@ text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), $this->text->e($assignee) ) ?> - text->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> + text->e($author), $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> dt->datetime($date_creation) ?>

    text->e($task['title']) ?>

    -
    \ No newline at end of file + diff --git a/sources/app/Template/event/task_close.php b/sources/app/Template/event/task_close.php index 04157b0..90ff920 100644 --- a/sources/app/Template/event/task_close.php +++ b/sources/app/Template/event/task_close.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    diff --git a/sources/app/Template/event/task_create.php b/sources/app/Template/event/task_create.php index 6e26cdb..017a5ad 100644 --- a/sources/app/Template/event/task_create.php +++ b/sources/app/Template/event/task_create.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    diff --git a/sources/app/Template/event/task_file_create.php b/sources/app/Template/event/task_file_create.php index 0d8e545..d329529 100644 --- a/sources/app/Template/event/task_file_create.php +++ b/sources/app/Template/event/task_file_create.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    diff --git a/sources/app/Template/event/task_move_column.php b/sources/app/Template/event/task_move_column.php index 0a78bca..f3155e4 100644 --- a/sources/app/Template/event/task_move_column.php +++ b/sources/app/Template/event/task_move_column.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), $this->text->e($task['column_title']) ) ?> dt->datetime($date_creation) ?> diff --git a/sources/app/Template/event/task_move_position.php b/sources/app/Template/event/task_move_position.php index eeadfa0..ecdd02b 100644 --- a/sources/app/Template/event/task_move_position.php +++ b/sources/app/Template/event/task_move_position.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), $task['position'], $this->text->e($task['column_title']) ) ?> diff --git a/sources/app/Template/event/task_move_swimlane.php b/sources/app/Template/event/task_move_swimlane.php index a190bc0..fe9bfb5 100644 --- a/sources/app/Template/event/task_move_swimlane.php +++ b/sources/app/Template/event/task_move_swimlane.php @@ -2,12 +2,12 @@ text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), $this->text->e($task['swimlane_name']) ) ?> diff --git a/sources/app/Template/event/task_open.php b/sources/app/Template/event/task_open.php index d9cd90b..548aa98 100644 --- a/sources/app/Template/event/task_open.php +++ b/sources/app/Template/event/task_open.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    diff --git a/sources/app/Template/event/task_update.php b/sources/app/Template/event/task_update.php index 07b94ff..7c7507c 100644 --- a/sources/app/Template/event/task_update.php +++ b/sources/app/Template/event/task_update.php @@ -1,7 +1,7 @@

    text->e($author), - $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + $this->url->link(t('#%d', $task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ) ?> dt->datetime($date_creation) ?>

    diff --git a/sources/app/Template/export/sidebar.php b/sources/app/Template/export/sidebar.php index 6a1de7e..55fbaee 100644 --- a/sources/app/Template/export/sidebar.php +++ b/sources/app/Template/export/sidebar.php @@ -1,18 +1,18 @@ \ No newline at end of file + diff --git a/sources/app/Template/feed/project.php b/sources/app/Template/feed/project.php index 1c6d116..213a04d 100644 --- a/sources/app/Template/feed/project.php +++ b/sources/app/Template/feed/project.php @@ -2,15 +2,15 @@ <?= t('%s\'s activity', $project['name']) ?> - + - url->href('feed', 'project', array('token' => $project['token']), false, '', true) ?> + url->href('FeedController', 'project', array('token' => $project['token']), false, '', true) ?> url->base() ?>assets/img/favicon.png <?= $e['event_title'] ?> - + @@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/sources/app/Template/feed/user.php b/sources/app/Template/feed/user.php index 28847f1..0c45f03 100644 --- a/sources/app/Template/feed/user.php +++ b/sources/app/Template/feed/user.php @@ -2,15 +2,15 @@ <?= t('Project activities for %s', $user['name'] ?: $user['username']) ?> - + - url->href('feed', 'user', array('token' => $user['token']), false, '', true) ?> + url->href('FeedController', 'user', array('token' => $user['token']), false, '', true) ?> url->base() ?>assets/img/favicon.png <?= $e['event_title'] ?> - + @@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/sources/app/Template/file_viewer/show.php b/sources/app/Template/file_viewer/show.php index 191aaa6..e829a17 100644 --- a/sources/app/Template/file_viewer/show.php +++ b/sources/app/Template/file_viewer/show.php @@ -3,7 +3,7 @@
    - <?= $this->text->e($file['name']) ?> + <?= $this->text->e($file['name']) ?>
    text->markdown($content) ?> @@ -11,4 +11,4 @@
    -
    \ No newline at end of file + diff --git a/sources/app/Template/gantt/task_creation.php b/sources/app/Template/gantt/task_creation.php deleted file mode 100644 index 448a808..0000000 --- a/sources/app/Template/gantt/task_creation.php +++ /dev/null @@ -1,35 +0,0 @@ - -
    - form->csrf() ?> - form->hidden('project_id', $values) ?> - form->hidden('column_id', $values) ?> - form->hidden('position', $values) ?> - -
    - form->label(t('Title'), 'title') ?> - form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?> - - form->label(t('Description'), 'description') ?> - form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"', 'tabindex="2"'), 'markdown-editor') ?> - - render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?> -
    - -
    - task->selectAssignee($users_list, $values, $errors) ?> - task->selectCategory($categories_list, $values, $errors) ?> - task->selectSwimlane($swimlanes_list, $values, $errors) ?> - task->selectPriority($project, $values) ?> - task->selectScore($values, $errors) ?> - task->selectStartDate($values, $errors) ?> - task->selectDueDate($values, $errors) ?> -
    - -
    - - - url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> -
    -
    diff --git a/sources/app/Template/group/associate.php b/sources/app/Template/group/associate.php index 9de46f3..8778756 100644 --- a/sources/app/Template/group/associate.php +++ b/sources/app/Template/group/associate.php @@ -1,25 +1,20 @@ -
    - - -

    - -
    - form->csrf() ?> - form->hidden('group_id', $values) ?> + + +

    + + + form->csrf() ?> + form->hidden('group_id', $values) ?> - form->label(t('User'), 'user_id') ?> - form->select('user_id', $users, $values, $errors, array('required'), 'chosen-select') ?> + form->label(t('User'), 'user_id') ?> + form->select('user_id', $users, $values, $errors, array('required'), 'chosen-select') ?> -
    - - - url->link(t('cancel'), 'group', 'index') ?> -
    -
    - -
    +
    + + + url->link(t('cancel'), 'GroupListController', 'index', array(), false, 'close-popover') ?> +
    + + diff --git a/sources/app/Template/group/create.php b/sources/app/Template/group/create.php deleted file mode 100644 index 4ce6b1f..0000000 --- a/sources/app/Template/group/create.php +++ /dev/null @@ -1,19 +0,0 @@ -
    - -
    - form->csrf() ?> - - form->label(t('Name'), 'name') ?> - form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?> - -
    - - - url->link(t('cancel'), 'group', 'index') ?> -
    -
    -
    diff --git a/sources/app/Template/group/dissociate.php b/sources/app/Template/group/dissociate.php index e1c6076..50ef6d6 100644 --- a/sources/app/Template/group/dissociate.php +++ b/sources/app/Template/group/dissociate.php @@ -1,17 +1,12 @@ -
    - -
    -

    + +
    +

    -
    - url->link(t('Yes'), 'group', 'removeUser', array('group_id' => $group['id'], 'user_id' => $user['id']), true, 'btn btn-red') ?> - - url->link(t('cancel'), 'group', 'users', array('group_id' => $group['id'])) ?> -
    +
    + url->link(t('Yes'), 'GroupListController', 'removeUser', array('group_id' => $group['id'], 'user_id' => $user['id']), true, 'btn btn-red') ?> + + url->link(t('cancel'), 'GroupListController', 'users', array('group_id' => $group['id']), false, 'close-popover') ?>
    -
    + diff --git a/sources/app/Template/group/edit.php b/sources/app/Template/group/edit.php deleted file mode 100644 index e9d9dd5..0000000 --- a/sources/app/Template/group/edit.php +++ /dev/null @@ -1,22 +0,0 @@ -
    - -
    - form->csrf() ?> - - form->hidden('id', $values) ?> - form->hidden('external_id', $values) ?> - - form->label(t('Name'), 'name') ?> - form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?> - -
    - - - url->link(t('cancel'), 'group', 'index') ?> -
    -
    -
    diff --git a/sources/app/Template/group/index.php b/sources/app/Template/group/index.php index 8302e5a..1062e18 100644 --- a/sources/app/Template/group/index.php +++ b/sources/app/Template/group/index.php @@ -1,8 +1,8 @@
    isEmpty()): ?> @@ -24,16 +24,16 @@ text->e($group['external_id']) ?> - text->e($group['name']) ?> + url->link($this->text->e($group['name']), 'GroupListController', 'users', array('group_id' => $group['id'])) ?> diff --git a/sources/app/Template/group/remove.php b/sources/app/Template/group/remove.php index 1cb007b..408b3d8 100644 --- a/sources/app/Template/group/remove.php +++ b/sources/app/Template/group/remove.php @@ -1,17 +1,12 @@ -
    - -
    -

    + +
    +

    -
    - url->link(t('Yes'), 'group', 'remove', array('group_id' => $group['id']), true, 'btn btn-red') ?> - - url->link(t('cancel'), 'group', 'index') ?> -
    +
    + url->link(t('Yes'), 'GroupListController', 'remove', array('group_id' => $group['id']), true, 'btn btn-red') ?> + + url->link(t('cancel'), 'GroupListController', 'index', array(), false, 'close-popover') ?>
    -
    + diff --git a/sources/app/Template/group/users.php b/sources/app/Template/group/users.php index bbd4152..a4895ab 100644 --- a/sources/app/Template/group/users.php +++ b/sources/app/Template/group/users.php @@ -1,8 +1,8 @@
    isEmpty()): ?> @@ -19,10 +19,10 @@ getCollection() as $user): ?> - url->link('#'.$user['id'], 'user', 'show', array('user_id' => $user['id'])) ?> + url->link('#'.$user['id'], 'UserViewController', 'show', array('user_id' => $user['id'])) ?> - url->link($this->text->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?> + url->link($this->text->e($user['username']), 'UserViewController', 'show', array('user_id' => $user['id'])) ?> text->e($user['name']) ?> @@ -31,7 +31,8 @@ text->e($user['email']) ?> - url->link(t('Remove this user'), 'group', 'dissociate', array('group_id' => $group['id'], 'user_id' => $user['id'])) ?> + + url->link(t('Remove this user'), 'GroupListController', 'dissociate', array('group_id' => $group['id'], 'user_id' => $user['id']), false, 'popover') ?> diff --git a/sources/app/Template/group_creation/show.php b/sources/app/Template/group_creation/show.php new file mode 100644 index 0000000..b219bd7 --- /dev/null +++ b/sources/app/Template/group_creation/show.php @@ -0,0 +1,15 @@ + +
    + form->csrf() ?> + + form->label(t('Name'), 'name') ?> + form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?> + +
    + + + url->link(t('cancel'), 'GroupListController', 'index', array(), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/group_modification/show.php b/sources/app/Template/group_modification/show.php new file mode 100644 index 0000000..ddf0736 --- /dev/null +++ b/sources/app/Template/group_modification/show.php @@ -0,0 +1,18 @@ + +
    + form->csrf() ?> + + form->hidden('id', $values) ?> + form->hidden('external_id', $values) ?> + + form->label(t('Name'), 'name') ?> + form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="100"')) ?> + +
    + + + url->link(t('cancel'), 'GroupListController', 'index', array(), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/header.php b/sources/app/Template/header.php index a8fd47f..13521ae 100644 --- a/sources/app/Template/header.php +++ b/sources/app/Template/header.php @@ -2,13 +2,17 @@
    diff --git a/sources/app/Template/project/remove.php b/sources/app/Template/project/remove.php deleted file mode 100644 index fa43fc7..0000000 --- a/sources/app/Template/project/remove.php +++ /dev/null @@ -1,14 +0,0 @@ - - -
    -

    - -

    - -
    - url->link(t('Yes'), 'project', 'remove', array('project_id' => $project['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> - url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> -
    -
    \ No newline at end of file diff --git a/sources/app/Template/project/share.php b/sources/app/Template/project/share.php deleted file mode 100644 index 6f66c97..0000000 --- a/sources/app/Template/project/share.php +++ /dev/null @@ -1,19 +0,0 @@ - - - - -
    -
      -
    • url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?>
    • -
    • url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?>
    • -
    • url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token']), false, '', '', true) ?>
    • -
    -
    - - url->link(t('Disable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> - - - url->link(t('Enable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> - diff --git a/sources/app/Template/project/sidebar.php b/sources/app/Template/project/sidebar.php index 304b4ae..d0f5059 100644 --- a/sources/app/Template/project/sidebar.php +++ b/sources/app/Template/project/sidebar.php @@ -1,62 +1,62 @@ - \ No newline at end of file + diff --git a/sources/app/Template/project_creation/create.php b/sources/app/Template/project_creation/create.php index c34173a..d00883b 100644 --- a/sources/app/Template/project_creation/create.php +++ b/sources/app/Template/project_creation/create.php @@ -2,7 +2,7 @@ -
    + form->csrf() ?> form->hidden('is_private', $values) ?> @@ -22,16 +22,17 @@ form->checkbox('projectPermission', t('Permissions'), 1, true) ?> - form->checkbox('category', t('Categories'), 1, true) ?> - form->checkbox('action', t('Actions'), 1, true) ?> - form->checkbox('swimlane', t('Swimlanes'), 1, true) ?> - form->checkbox('task', t('Tasks'), 1, false) ?> + form->checkbox('categoryModel', t('Categories'), 1, true) ?> + form->checkbox('tagDuplicationModel', t('Tags'), 1, true) ?> + form->checkbox('actionModel', t('Actions'), 1, true) ?> + form->checkbox('swimlaneModel', t('Swimlanes'), 1, true) ?> + form->checkbox('projectTaskDuplicationModel', t('Tasks'), 1, false) ?>
    - url->link(t('cancel'), 'project', 'index', array(), false, 'close-popover') ?> + url->link(t('cancel'), 'ProjectListController', 'show', array(), false, 'close-popover') ?>
    @@ -39,4 +40,4 @@

    -
    \ No newline at end of file + diff --git a/sources/app/Template/project_edit/dates.php b/sources/app/Template/project_edit/dates.php index f77b726..48135dd 100644 --- a/sources/app/Template/project_edit/dates.php +++ b/sources/app/Template/project_edit/dates.php @@ -1,13 +1,13 @@ -
    + form->csrf() ?> form->hidden('id', $values) ?> form->hidden('name', $values) ?> diff --git a/sources/app/Template/project_edit/description.php b/sources/app/Template/project_edit/description.php index e6c42ca..f7e7be4 100644 --- a/sources/app/Template/project_edit/description.php +++ b/sources/app/Template/project_edit/description.php @@ -1,13 +1,13 @@ - + form->csrf() ?> form->hidden('id', $values) ?> form->hidden('name', $values) ?> diff --git a/sources/app/Template/project_edit/general.php b/sources/app/Template/project_edit/general.php index 6c3fa68..c742147 100644 --- a/sources/app/Template/project_edit/general.php +++ b/sources/app/Template/project_edit/general.php @@ -1,13 +1,13 @@ - + form->csrf() ?> form->hidden('id', $values) ?> @@ -24,7 +24,7 @@ form->select('owner_id', $owners, $values, $errors) ?> - user->hasProjectAccess('ProjectCreation', 'create', $project['id'])): ?> + user->hasProjectAccess('ProjectCreationController', 'create', $project['id'])): ?>
    form->checkbox('is_private', t('Private project'), 1, $project['is_private'] == 1) ?>

    diff --git a/sources/app/Template/project_edit/task_priority.php b/sources/app/Template/project_edit/task_priority.php index 56c2575..3ef4b3c 100644 --- a/sources/app/Template/project_edit/task_priority.php +++ b/sources/app/Template/project_edit/task_priority.php @@ -1,13 +1,13 @@ - + form->csrf() ?> form->hidden('id', $values) ?> form->hidden('name', $values) ?> diff --git a/sources/app/Template/project_file/create.php b/sources/app/Template/project_file/create.php index 6731528..e262799 100644 --- a/sources/app/Template/project_file/create.php +++ b/sources/app/Template/project_file/create.php @@ -4,7 +4,7 @@ @@ -18,7 +18,7 @@
    + data-url="url->href('ProjectFileController', 'save', array('project_id' => $project['id'])) ?>">
    @@ -27,7 +27,7 @@
    - + - url->link(t('cancel'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'ProjectOverviewController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/project_file/remove.php b/sources/app/Template/project_file/remove.php index ba83428..0517a9e 100644 --- a/sources/app/Template/project_file/remove.php +++ b/sources/app/Template/project_file/remove.php @@ -8,8 +8,8 @@

    - url->link(t('Yes'), 'ProjectFile', 'remove', array('project_id' => $project['id'], 'file_id' => $file['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'ProjectFileController', 'remove', array('project_id' => $project['id'], 'file_id' => $file['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'ProjectOverviewController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/gantt/projects.php b/sources/app/Template/project_gantt/show.php similarity index 67% rename from sources/app/Template/gantt/projects.php rename to sources/app/Template/project_gantt/show.php index 84b260b..af22a6e 100644 --- a/sources/app/Template/gantt/projects.php +++ b/sources/app/Template/project_gantt/show.php @@ -2,10 +2,10 @@ @@ -16,9 +16,9 @@
    board->isCollapsed($project['id']) ? '' : 'style="display: none;"' ?>> - url->link(t('Expand tasks'), 'board', 'expand', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> + url->link(t('Expand tasks'), 'BoardAjaxController', 'expand', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> board->isCollapsed($project['id']) ? 'style="display: none;"' : '' ?>> - url->link(t('Collapse tasks'), 'board', 'collapse', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> + url->link(t('Collapse tasks'), 'BoardAjaxController', 'collapse', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
  • @@ -29,6 +29,66 @@
  • - render('project/dropdown', array('project' => $project)) ?> + + user->hasProjectAccess('TaskCreationController', 'show', $project['id'])): ?> +
  • + + url->link(t('Add a new task'), 'TaskCreationController', 'show', array('project_id' => $project['id']), false, 'popover') ?> +
  • + + +
  • + + url->link(t('Activity'), 'ActivityController', 'project', array('project_id' => $project['id'])) ?> +
  • + + user->hasProjectAccess('CustomFilterController', 'index', $project['id'])): ?> +
  • + + url->link(t('Custom filters'), 'CustomFilterController', 'index', array('project_id' => $project['id'])) ?> +
  • + + + +
  • + + url->link(t('Public link'), 'BoardViewController', 'readonly', array('token' => $project['token']), false, '', '', true) ?> +
  • + + + hook->render('template:project:dropdown', array('project' => $project)) ?> + + user->hasProjectAccess('AnalyticController', 'tasks', $project['id'])): ?> +
  • + + url->link(t('Analytics'), 'AnalyticController', 'tasks', array('project_id' => $project['id'])) ?> +
  • + + + user->hasProjectAccess('ExportController', 'tasks', $project['id'])): ?> +
  • + + url->link(t('Exports'), 'ExportController', 'tasks', array('project_id' => $project['id'])) ?> +
  • + + + user->hasProjectAccess('TaskImportController', 'tasks', $project['id'])): ?> +
  • + + url->link(t('Imports'), 'TaskImportController', 'show', array('project_id' => $project['id'])) ?> +
  • + + + user->hasProjectAccess('ProjectEditController', 'edit', $project['id'])): ?> +
  • + + url->link(t('Settings'), 'ProjectViewController', 'show', array('project_id' => $project['id'])) ?> +
  • + + +
  • + + url->link(t('Manage projects'), 'ProjectListController', 'show') ?> +
  • -
    \ No newline at end of file + diff --git a/sources/app/Template/project_header/search.php b/sources/app/Template/project_header/search.php index 4221635..8885d9c 100644 --- a/sources/app/Template/project_header/search.php +++ b/sources/app/Template/project_header/search.php @@ -22,9 +22,9 @@ @@ -34,9 +34,9 @@ diff --git a/sources/app/Template/project_header/views.php b/sources/app/Template/project_header/views.php index 353e4b6..3a41c91 100644 --- a/sources/app/Template/project_header/views.php +++ b/sources/app/Template/project_header/views.php @@ -1,24 +1,24 @@
      -
    • app->getRouterController() === 'ProjectOverview' ? 'class="active"' : '' ?>> +
    • app->checkMenuSelection('ProjectOverviewController') ?>> - url->link(t('Overview'), 'ProjectOverview', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?> + url->link(t('Overview'), 'ProjectOverviewController', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?>
    • -
    • app->getRouterController() === 'Board' ? 'class="active"' : '' ?>> +
    • app->checkMenuSelection('BoardViewController') ?>> - url->link(t('Board'), 'board', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-board', t('Keyboard shortcut: "%s"', 'v b')) ?> + url->link(t('Board'), 'BoardViewController', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-board', t('Keyboard shortcut: "%s"', 'v b')) ?>
    • -
    • app->getRouterController() === 'Calendar' ? 'class="active"' : '' ?>> +
    • app->checkMenuSelection('Calendar') ?>> - url->link(t('Calendar'), 'calendar', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-calendar', t('Keyboard shortcut: "%s"', 'v c')) ?> + url->link(t('Calendar'), 'CalendarController', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-calendar', t('Keyboard shortcut: "%s"', 'v c')) ?>
    • -
    • app->getRouterController() === 'Listing' ? 'class="active"' : '' ?>> +
    • app->checkMenuSelection('TaskListController') ?>> - url->link(t('List'), 'listing', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-listing', t('Keyboard shortcut: "%s"', 'v l')) ?> + url->link(t('List'), 'TaskListController', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-listing', t('Keyboard shortcut: "%s"', 'v l')) ?>
    • - user->hasProjectAccess('gantt', 'project', $project['id'])): ?> -
    • app->getRouterController() === 'Gantt' ? 'class="active"' : '' ?>> + user->hasProjectAccess('TaskGanttController', 'show', $project['id'])): ?> +
    • app->checkMenuSelection('TaskGanttController') ?>> - url->link(t('Gantt'), 'gantt', 'project', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-gantt', t('Keyboard shortcut: "%s"', 'v g')) ?> + url->link(t('Gantt'), 'TaskGanttController', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-gantt', t('Keyboard shortcut: "%s"', 'v g')) ?>
    • -
    \ No newline at end of file + diff --git a/sources/app/Template/project/index.php b/sources/app/Template/project_list/show.php similarity index 70% rename from sources/app/Template/project/index.php rename to sources/app/Template/project_list/show.php index 10d4aaa..8b9f139 100644 --- a/sources/app/Template/project/index.php +++ b/sources/app/Template/project_list/show.php @@ -1,11 +1,11 @@
    @@ -20,7 +20,7 @@ order(t('Start date'), 'start_date') ?> order(t('End date'), 'end_date') ?> order(t('Owner'), 'owner_id') ?> - user->hasAccess('projectuser', 'managers')): ?> + user->hasAccess('ProjectUserOverviewController', 'managers')): ?> @@ -28,7 +28,7 @@ getCollection() as $project): ?> - url->link('#'.$project['id'], 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link') ?> + render('project/dropdown', array('project' => $project)) ?> @@ -38,8 +38,7 @@ - url->link('', 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Board')) ?> - url->link('', 'gantt', 'project', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?> + url->link($this->text->e($project['name']), 'BoardViewController', 'show', array('project_id' => $project['id'])) ?> @@ -49,12 +48,10 @@ - '> + - - url->link($this->text->e($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?> dt->date($project['start_date']) ?> @@ -67,10 +64,10 @@ text->e($project['owner_name'] ?: $project['owner_username']) ?> - user->hasAccess('projectuser', 'managers')): ?> + user->hasAccess('ProjectUserOverviewController', 'managers')): ?> - + diff --git a/sources/app/Template/project_overview/attachments.php b/sources/app/Template/project_overview/attachments.php index eaabfd5..ab8cf2a 100644 --- a/sources/app/Template/project_overview/attachments.php +++ b/sources/app/Template/project_overview/attachments.php @@ -3,9 +3,9 @@

    - user->hasProjectAccess('ProjectFile', 'create', $project['id'])): ?> + user->hasProjectAccess('ProjectFileController', 'create', $project['id'])): ?>
    - url->button('fa-plus', t('Upload a file'), 'ProjectFile', 'create', array('project_id' => $project['id']), 'popover') ?> + url->button('fa-plus', t('Upload a file'), 'ProjectFileController', 'create', array('project_id' => $project['id']), 'popover') ?>
    diff --git a/sources/app/Template/project_overview/description.php b/sources/app/Template/project_overview/description.php index 72ccf06..0c2027e 100644 --- a/sources/app/Template/project_overview/description.php +++ b/sources/app/Template/project_overview/description.php @@ -3,9 +3,9 @@

    - user->hasProjectAccess('ProjectEdit', 'description', $project['id'])): ?> + user->hasProjectAccess('ProjectEditController', 'description', $project['id'])): ?>
    - url->button('fa-edit', t('Edit description'), 'ProjectEdit', 'description', array('project_id' => $project['id']), 'popover') ?> + url->button('fa-edit', t('Edit description'), 'ProjectEditController', 'description', array('project_id' => $project['id']), 'popover') ?>
    diff --git a/sources/app/Template/project_overview/files.php b/sources/app/Template/project_overview/files.php index c570b77..fa87093 100644 --- a/sources/app/Template/project_overview/files.php +++ b/sources/app/Template/project_overview/files.php @@ -16,17 +16,17 @@ file->getPreviewType($file['name']) !== null): ?>
  • - url->link(t('View file'), 'FileViewer', 'show', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?> + url->link(t('View file'), 'FileViewerController', 'show', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?>
  • - url->link(t('Download'), 'FileViewer', 'download', array('project_id' => $project['id'], 'file_id' => $file['id'])) ?> + url->link(t('Download'), 'FileViewerController', 'download', array('project_id' => $project['id'], 'file_id' => $file['id'])) ?>
  • - user->hasProjectAccess('ProjectFile', 'remove', $project['id'])): ?> + user->hasProjectAccess('ProjectFileController', 'remove', $project['id'])): ?>
  • - url->link(t('Remove'), 'ProjectFile', 'confirm', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?> + url->link(t('Remove'), 'ProjectFileController', 'confirm', array('project_id' => $project['id'], 'file_id' => $file['id']), false, 'popover') ?>
  • diff --git a/sources/app/Template/project_overview/images.php b/sources/app/Template/project_overview/images.php index f6937e1..7f38e2b 100644 --- a/sources/app/Template/project_overview/images.php +++ b/sources/app/Template/project_overview/images.php @@ -2,7 +2,7 @@
    - <?= $this->text->e($file['name']) ?> + <?= $this->text->e($file['name']) ?>
    diff --git a/sources/app/Template/project_overview/show.php b/sources/app/Template/project_overview/show.php index 6fe815b..6b2bc2c 100644 --- a/sources/app/Template/project_overview/show.php +++ b/sources/app/Template/project_overview/show.php @@ -1,5 +1,5 @@
    - projectHeader->render($project, 'ProjectOverview', 'show') ?> + projectHeader->render($project, 'ProjectOverviewController', 'show') ?> render('project_overview/columns', array('project' => $project)) ?> render('project_overview/description', array('project' => $project)) ?> render('project_overview/attachments', array('project' => $project, 'images' => $images, 'files' => $files)) ?> diff --git a/sources/app/Template/project_permission/index.php b/sources/app/Template/project_permission/index.php index a7d666a..d850ec5 100644 --- a/sources/app/Template/project_permission/index.php +++ b/sources/app/Template/project_permission/index.php @@ -26,12 +26,12 @@ $roles, array('role-'.$user['id'] => $user['role']), array(), - array('data-url="'.$this->url->href('ProjectPermission', 'changeUserRole', array('project_id' => $project['id'])).'"', 'data-id="'.$user['id'].'"'), + array('data-url="'.$this->url->href('ProjectPermissionController', 'changeUserRole', array('project_id' => $project['id'])).'"', 'data-id="'.$user['id'].'"'), 'project-change-role' ) ?> - url->link(t('Remove'), 'ProjectPermission', 'removeUser', array('project_id' => $project['id'], 'user_id' => $user['id']), true) ?> + url->link(t('Remove'), 'ProjectPermissionController', 'removeUser', array('project_id' => $project['id'], 'user_id' => $user['id']), true) ?> @@ -40,7 +40,7 @@
    - + form->csrf() ?> form->hidden('project_id', array('project_id' => $project['id'])) ?> form->hidden('user_id', $values) ?> @@ -51,7 +51,7 @@ 'placeholder="'.t('Enter user name...').'"', 'title="'.t('Enter user name...').'"', 'data-dst-field="user_id"', - 'data-search-url="'.$this->url->href('UserHelper', 'autocomplete').'"', + 'data-search-url="'.$this->url->href('UserAjaxController', 'autocomplete').'"', ), 'autocomplete') ?> @@ -86,12 +86,12 @@ $roles, array('role-'.$group['id'] => $group['role']), array(), - array('data-url="'.$this->url->href('ProjectPermission', 'changeGroupRole', array('project_id' => $project['id'])).'"', 'data-id="'.$group['id'].'"'), + array('data-url="'.$this->url->href('ProjectPermissionController', 'changeGroupRole', array('project_id' => $project['id'])).'"', 'data-id="'.$group['id'].'"'), 'project-change-role' ) ?> - url->link(t('Remove'), 'ProjectPermission', 'removeGroup', array('project_id' => $project['id'], 'group_id' => $group['id']), true) ?> + url->link(t('Remove'), 'ProjectPermissionController', 'removeGroup', array('project_id' => $project['id'], 'group_id' => $group['id']), true) ?> @@ -100,7 +100,7 @@
    - + form->csrf() ?> form->hidden('project_id', array('project_id' => $project['id'])) ?> form->hidden('group_id', $values) ?> @@ -113,7 +113,7 @@ 'title="'.t('Enter group name...').'"', 'data-dst-field="group_id"', 'data-dst-extra-field="external_id"', - 'data-search-url="'.$this->url->href('GroupHelper', 'autocomplete').'"', + 'data-search-url="'.$this->url->href('GroupAjaxController', 'autocomplete').'"', ), 'autocomplete') ?> @@ -128,7 +128,7 @@
    - + form->csrf() ?> form->hidden('id', array('id' => $project['id'])) ?> diff --git a/sources/app/Template/project_status/disable.php b/sources/app/Template/project_status/disable.php new file mode 100644 index 0000000..d8145d3 --- /dev/null +++ b/sources/app/Template/project_status/disable.php @@ -0,0 +1,14 @@ + + +
    +

    + +

    + +
    + url->link(t('Yes'), 'ProjectStatusController', 'disable', array('project_id' => $project['id']), true, 'btn btn-red') ?> + url->link(t('cancel'), 'ProjectViewController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/project_status/enable.php b/sources/app/Template/project_status/enable.php new file mode 100644 index 0000000..1f76d09 --- /dev/null +++ b/sources/app/Template/project_status/enable.php @@ -0,0 +1,14 @@ + + +
    +

    + +

    + +
    + url->link(t('Yes'), 'ProjectStatusController', 'enable', array('project_id' => $project['id']), true, 'btn btn-red') ?> + url->link(t('cancel'), 'ProjectViewController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/project_status/remove.php b/sources/app/Template/project_status/remove.php new file mode 100644 index 0000000..8959ef7 --- /dev/null +++ b/sources/app/Template/project_status/remove.php @@ -0,0 +1,14 @@ + + +
    +

    + +

    + +
    + url->link(t('Yes'), 'ProjectStatusController', 'remove', array('project_id' => $project['id']), true, 'btn btn-red') ?> + url->link(t('cancel'), 'ProjectViewController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/project_tag/create.php b/sources/app/Template/project_tag/create.php new file mode 100644 index 0000000..bfd1084 --- /dev/null +++ b/sources/app/Template/project_tag/create.php @@ -0,0 +1,16 @@ + + + form->csrf() ?> + form->hidden('project_id', $values) ?> + + form->label(t('Name'), 'name') ?> + form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="255"')) ?> + +
    + + + url->link(t('cancel'), 'ProjectTagController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> +
    + diff --git a/sources/app/Template/project_tag/edit.php b/sources/app/Template/project_tag/edit.php new file mode 100644 index 0000000..9bf261b --- /dev/null +++ b/sources/app/Template/project_tag/edit.php @@ -0,0 +1,17 @@ + +
    + form->csrf() ?> + form->hidden('id', $values) ?> + form->hidden('project_id', $values) ?> + + form->label(t('Name'), 'name') ?> + form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="255"')) ?> + +
    + + + url->link(t('cancel'), 'ProjectTagController', 'index', array(), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/project_tag/index.php b/sources/app/Template/project_tag/index.php new file mode 100644 index 0000000..8e8dd96 --- /dev/null +++ b/sources/app/Template/project_tag/index.php @@ -0,0 +1,31 @@ + + + +

    + + + + + + + + + + + + +
    text->e($tag['name']) ?> + + url->link(t('Remove'), 'ProjectTagController', 'confirm', array('tag_id' => $tag['id'], 'project_id' => $project['id']), false, 'popover') ?> + + url->link(t('Edit'), 'ProjectTagController', 'edit', array('tag_id' => $tag['id'], 'project_id' => $project['id']), false, 'popover') ?> +
    + diff --git a/sources/app/Template/project_tag/remove.php b/sources/app/Template/project_tag/remove.php new file mode 100644 index 0000000..f4aadab --- /dev/null +++ b/sources/app/Template/project_tag/remove.php @@ -0,0 +1,15 @@ + + +
    +

    + +

    + +
    + url->link(t('Yes'), 'ProjectTagController', 'remove', array('tag_id' => $tag['id'], 'project_id' => $project['id']), true, 'btn btn-red popover-link') ?> + + url->link(t('cancel'), 'ProjectTagController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/project_user/sidebar.php b/sources/app/Template/project_user/sidebar.php deleted file mode 100644 index ff113eb..0000000 --- a/sources/app/Template/project_user/sidebar.php +++ /dev/null @@ -1,30 +0,0 @@ - \ No newline at end of file diff --git a/sources/app/Template/project_user/layout.php b/sources/app/Template/project_user_overview/layout.php similarity index 75% rename from sources/app/Template/project_user/layout.php rename to sources/app/Template/project_user_overview/layout.php index 1103e9b..19b8343 100644 --- a/sources/app/Template/project_user/layout.php +++ b/sources/app/Template/project_user_overview/layout.php @@ -3,12 +3,12 @@
    • - url->link(t('Projects list'), 'project', 'index') ?> + url->link(t('Projects list'), 'ProjectListController', 'show') ?>
    • - user->hasAccess('gantt', 'projects')): ?> + user->hasAccess('ProjectGanttController', 'show')): ?>
    • - url->link(t('Projects Gantt chart'), 'gantt', 'projects') ?> + url->link(t('Projects Gantt chart'), 'ProjectGanttController', 'show') ?>
    @@ -24,4 +24,4 @@
    -
    \ No newline at end of file + diff --git a/sources/app/Template/project_user/roles.php b/sources/app/Template/project_user_overview/roles.php similarity index 76% rename from sources/app/Template/project_user/roles.php rename to sources/app/Template/project_user_overview/roles.php index 17fb709..87c8df8 100644 --- a/sources/app/Template/project_user/roles.php +++ b/sources/app/Template/project_user_overview/roles.php @@ -13,9 +13,9 @@ text->e($this->user->getFullname($project)) ?> - url->link('', 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Board')) ?> - url->link('', 'gantt', 'project', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?> - url->link('', 'project', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Project settings')) ?> + url->link('', 'BoardViewController', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Board')) ?> + url->link('', 'TaskGanttController', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?> + url->link('', 'ProjectViewController', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Project settings')) ?> text->e($project['project_name']) ?> @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/sources/app/Template/project_user_overview/sidebar.php b/sources/app/Template/project_user_overview/sidebar.php new file mode 100644 index 0000000..9a87d4e --- /dev/null +++ b/sources/app/Template/project_user_overview/sidebar.php @@ -0,0 +1,30 @@ + diff --git a/sources/app/Template/project_user/tasks.php b/sources/app/Template/project_user_overview/tasks.php similarity index 83% rename from sources/app/Template/project_user/tasks.php rename to sources/app/Template/project_user_overview/tasks.php index 108d3b3..af0a3d9 100644 --- a/sources/app/Template/project_user/tasks.php +++ b/sources/app/Template/project_user_overview/tasks.php @@ -14,16 +14,16 @@ getCollection() as $task): ?> - url->link('#'.$this->text->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + url->link('#'.$this->text->e($task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> - url->link($this->text->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + url->link($this->text->e($task['project_name']), 'BoardViewController', 'show', array('project_id' => $task['project_id'])) ?> text->e($task['column_name']) ?> - url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> diff --git a/sources/app/Template/project_user/tooltip_users.php b/sources/app/Template/project_user_overview/tooltip_users.php similarity index 55% rename from sources/app/Template/project_user/tooltip_users.php rename to sources/app/Template/project_user_overview/tooltip_users.php index f75d964..7117a87 100644 --- a/sources/app/Template/project_user/tooltip_users.php +++ b/sources/app/Template/project_user_overview/tooltip_users.php @@ -1,14 +1,16 @@

    + $role_name): ?> - -
      +
    $user): ?> -
  • url->link($this->text->e($user), 'Projectuser', 'opens', array('user_id' => $user_id)) ?>
  • + - - \ No newline at end of file +
    + url->link($this->text->e($user), 'ProjectUserOverviewController', 'opens', array('user_id' => $user_id)) ?> +
    + diff --git a/sources/app/Template/project_view/duplicate.php b/sources/app/Template/project_view/duplicate.php new file mode 100644 index 0000000..d66ff59 --- /dev/null +++ b/sources/app/Template/project_view/duplicate.php @@ -0,0 +1,29 @@ + + +
    +

    + +

    +
    + + form->csrf() ?> + + + form->checkbox('projectPermission', t('Permissions'), 1, true) ?> + + + form->checkbox('categoryModel', t('Categories'), 1, true) ?> + form->checkbox('tagDuplicationModel', t('Tags'), 1, true) ?> + form->checkbox('actionModel', t('Actions'), 1, true) ?> + form->checkbox('swimlaneModel', t('Swimlanes'), 1, false) ?> + form->checkbox('projectMetadataModel', t('Metadata'), 1, false) ?> + form->checkbox('projectTaskDuplicationModel', t('Tasks'), 1, false) ?> + +
    + + url->link(t('cancel'), 'ProjectViewController', 'show', array('project_id' => $project['id'])) ?> +
    +
    +
    diff --git a/sources/app/Template/project/integrations.php b/sources/app/Template/project_view/integrations.php similarity index 74% rename from sources/app/Template/project/integrations.php rename to sources/app/Template/project_view/integrations.php index 54720c6..f8bff7e 100644 --- a/sources/app/Template/project/integrations.php +++ b/sources/app/Template/project_view/integrations.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> hook->render('template:project:integrations', array('project' => $project, 'values' => $values, 'webhook_token' => $webhook_token)) ?> @@ -12,4 +12,4 @@ -
    \ No newline at end of file + diff --git a/sources/app/Template/project/notifications.php b/sources/app/Template/project_view/notifications.php similarity index 67% rename from sources/app/Template/project/notifications.php rename to sources/app/Template/project_view/notifications.php index 494a322..29cc088 100644 --- a/sources/app/Template/project/notifications.php +++ b/sources/app/Template/project_view/notifications.php @@ -4,7 +4,7 @@

    -
    + form->csrf() ?> @@ -14,7 +14,7 @@
    - url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> + url->link(t('cancel'), 'ProjectViewController', 'show', array('project_id' => $project['id'])) ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/project_view/share.php b/sources/app/Template/project_view/share.php new file mode 100644 index 0000000..409f37e --- /dev/null +++ b/sources/app/Template/project_view/share.php @@ -0,0 +1,18 @@ + + + + +
    +
      +
    • url->link(t('Public link'), 'BoardViewController', 'readonly', array('token' => $project['token']), false, '', '', true) ?>
    • +
    • url->link(t('RSS feed'), 'FeedController', 'project', array('token' => $project['token']), false, '', '', true) ?>
    • +
    • url->link(t('iCal feed'), 'ICalendarController', 'project', array('token' => $project['token']), false, '', '', true) ?>
    • +
    +
    + + url->link(t('Disable public access'), 'ProjectViewController', 'updateSharing', array('project_id' => $project['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> + + url->link(t('Enable public access'), 'ProjectViewController', 'updateSharing', array('project_id' => $project['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> + diff --git a/sources/app/Template/project/show.php b/sources/app/Template/project_view/show.php similarity index 79% rename from sources/app/Template/project/show.php rename to sources/app/Template/project_view/show.php index 42eeec4..5efe8ce 100644 --- a/sources/app/Template/project/show.php +++ b/sources/app/Template/project_view/show.php @@ -13,9 +13,9 @@ -
  • url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?>
  • -
  • url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?>
  • -
  • url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?>
  • +
  • url->link(t('Public link'), 'BoardViewController', 'readonly', array('token' => $project['token']), false, '', '', true) ?>
  • +
  • url->link(t('RSS feed'), 'FeedController', 'project', array('token' => $project['token']), false, '', '', true) ?>
  • +
  • url->link(t('iCal feed'), 'ICalendarController', 'project', array('token' => $project['token'])) ?>
  • @@ -35,11 +35,11 @@ 0): ?> 0): ?> -
  • url->link(t('%d tasks on the board', $stats['nb_active_tasks']), 'board', 'show', array('project_id' => $project['id'], 'search' => 'status:open')) ?>
  • +
  • url->link(t('%d tasks on the board', $stats['nb_active_tasks']), 'BoardViewController', 'show', array('project_id' => $project['id'], 'search' => 'status:open')) ?>
  • 0): ?> -
  • url->link(t('%d closed tasks', $stats['nb_inactive_tasks']), 'listing', 'show', array('project_id' => $project['id'], 'search' => 'status:closed')) ?>
  • +
  • url->link(t('%d closed tasks', $stats['nb_inactive_tasks']), 'TaskListController', 'show', array('project_id' => $project['id'], 'search' => 'status:closed')) ?>
  • @@ -63,7 +63,7 @@ text->e($column['title']) ?> - '> + diff --git a/sources/app/Template/search/activity.php b/sources/app/Template/search/activity.php new file mode 100644 index 0000000..9abc7d7 --- /dev/null +++ b/sources/app/Template/search/activity.php @@ -0,0 +1,39 @@ +
    + + +
    + +
    + + +
    +

    +

    project:"My project" creator:me

    +
      +
    • project:"My project"
    • +
    • creator:admin
    • +
    • created:today
    • +
    • status:open
    • +
    • title:"My task"
    • +
    +

    url->doc(t('View advanced search syntax'), 'search') ?>

    +
    + +

    + + render('event/events', array('events' => $events)) ?> + + +
    diff --git a/sources/app/Template/search/index.php b/sources/app/Template/search/index.php index 9231a6f..bc528af 100644 --- a/sources/app/Template/search/index.php +++ b/sources/app/Template/search/index.php @@ -2,8 +2,8 @@ @@ -40,4 +40,4 @@ )) ?> - \ No newline at end of file + diff --git a/sources/app/Template/search/results.php b/sources/app/Template/search/results.php index 79df354..8376b9e 100644 --- a/sources/app/Template/search/results.php +++ b/sources/app/Template/search/results.php @@ -13,10 +13,10 @@ getCollection() as $task): ?> - url->link($this->text->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + url->link($this->text->e($task['project_name']), 'BoardViewController', 'show', array('project_id' => $task['project_id'])) ?> - url->link('#'.$this->text->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + url->link('#'.$this->text->e($task['id']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> text->e($task['swimlane_name'] ?: $task['default_swimlane']) ?> @@ -28,7 +28,7 @@ text->e($task['category_name']) ?> - url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> @@ -41,7 +41,7 @@ dt->date($task['date_due']) ?> - + diff --git a/sources/app/Template/subtask/create.php b/sources/app/Template/subtask/create.php index 029fddf..3c080f7 100644 --- a/sources/app/Template/subtask/create.php +++ b/sources/app/Template/subtask/create.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> form->hidden('task_id', $values) ?> @@ -15,6 +15,6 @@
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/subtask/edit.php b/sources/app/Template/subtask/edit.php index 3c210f6..8f256ce 100644 --- a/sources/app/Template/subtask/edit.php +++ b/sources/app/Template/subtask/edit.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> form->hidden('id', $values) ?> @@ -15,6 +15,6 @@
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/subtask/menu.php b/sources/app/Template/subtask/menu.php index 6c98b95..d5d1bf8 100644 --- a/sources/app/Template/subtask/menu.php +++ b/sources/app/Template/subtask/menu.php @@ -2,10 +2,16 @@
    • - url->link(t('Edit'), 'subtask', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?> + + url->link(t('Edit'), 'SubtaskController', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?>
    • - url->link(t('Remove'), 'subtask', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?> + + url->link(t('Remove'), 'SubtaskController', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?> +
    • +
    • + + url->link(t('Convert to task'), 'SubtaskConverterController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), false, 'popover') ?>
    diff --git a/sources/app/Template/subtask/remove.php b/sources/app/Template/subtask/remove.php index 374256f..426c1a9 100644 --- a/sources/app/Template/subtask/remove.php +++ b/sources/app/Template/subtask/remove.php @@ -3,15 +3,18 @@
    -

    +

    -

    - -

    text->e($subtask['title']) ?>

    +
      +
    • + text->e($subtask['title']) ?> +
    • +
    +
    - url->link(t('Yes'), 'subtask', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'SubtaskController', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/subtask/table.php b/sources/app/Template/subtask/table.php index 40510a2..4c6484e 100644 --- a/sources/app/Template/subtask/table.php +++ b/sources/app/Template/subtask/table.php @@ -1,7 +1,7 @@ @@ -44,11 +44,11 @@
  • - url->link(t('Stop timer'), 'SubtaskStatus', 'timer', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']), false, 'subtask-toggle-timer') ?> + url->link(t('Stop timer'), 'SubtaskStatusController', 'timer', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']), false, 'subtask-toggle-timer') ?> (dt->age($subtask['timer_start_date']) ?>) - url->link(t('Start timer'), 'SubtaskStatus', 'timer', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']), false, 'subtask-toggle-timer') ?> + url->link(t('Start timer'), 'SubtaskStatusController', 'timer', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id']), false, 'subtask-toggle-timer') ?>
  • diff --git a/sources/app/Template/subtask_converter/show.php b/sources/app/Template/subtask_converter/show.php new file mode 100644 index 0000000..63f4548 --- /dev/null +++ b/sources/app/Template/subtask_converter/show.php @@ -0,0 +1,20 @@ + + +
    +
    + +
      +
    • + text->e($subtask['title']) ?> +
    • +
    +
    + +
    + url->link(t('Yes'), 'SubtaskConverterController', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), true, 'btn btn-red') ?> + + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/subtask_restriction/popover.php b/sources/app/Template/subtask_restriction/show.php similarity index 62% rename from sources/app/Template/subtask_restriction/popover.php rename to sources/app/Template/subtask_restriction/show.php index 916a664..ec8b8d5 100644 --- a/sources/app/Template/subtask_restriction/popover.php +++ b/sources/app/Template/subtask_restriction/show.php @@ -1,7 +1,7 @@ - +form->csrf() ?> @@ -12,6 +12,6 @@
    - + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/swimlane/create.php b/sources/app/Template/swimlane/create.php index a9b4374..f5aa159 100644 --- a/sources/app/Template/swimlane/create.php +++ b/sources/app/Template/swimlane/create.php @@ -1,7 +1,7 @@ - +form->csrf() ?> form->hidden('project_id', $values) ?> @@ -15,6 +15,6 @@
    - url->link(t('cancel'), 'Swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'SwimlaneController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/swimlane/edit.php b/sources/app/Template/swimlane/edit.php index b08099a..b10cdd5 100644 --- a/sources/app/Template/swimlane/edit.php +++ b/sources/app/Template/swimlane/edit.php @@ -2,7 +2,7 @@

    - +form->csrf() ?> @@ -18,6 +18,6 @@
    - url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'SwimlaneController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/swimlane/edit_default.php b/sources/app/Template/swimlane/edit_default.php index 3bf8256..f271c51 100644 --- a/sources/app/Template/swimlane/edit_default.php +++ b/sources/app/Template/swimlane/edit_default.php @@ -1,7 +1,7 @@ - +form->csrf() ?> form->hidden('id', $values) ?> @@ -13,6 +13,6 @@
    - url->link(t('cancel'), 'Swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'SwimlaneController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/swimlane/index.php b/sources/app/Template/swimlane/index.php index fad3530..4f78a40 100644 --- a/sources/app/Template/swimlane/index.php +++ b/sources/app/Template/swimlane/index.php @@ -3,7 +3,7 @@
    • - url->link(t('Add a new swimlane'), 'Swimlane', 'create', array('project_id' => $project['id']), false, 'popover') ?> + url->link(t('Add a new swimlane'), 'SwimlaneController', 'create', array('project_id' => $project['id']), false, 'popover') ?>
    diff --git a/sources/app/Template/swimlane/remove.php b/sources/app/Template/swimlane/remove.php index 9be39ff..f16b778 100644 --- a/sources/app/Template/swimlane/remove.php +++ b/sources/app/Template/swimlane/remove.php @@ -9,9 +9,9 @@

    - url->link(t('Yes'), 'swimlane', 'remove', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'SwimlaneController', 'remove', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id']), false, 'close-popover') ?> + url->link(t('cancel'), 'SwimlaneController', 'index', array('project_id' => $project['id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/swimlane/table.php b/sources/app/Template/swimlane/table.php index 17be692..be123b0 100644 --- a/sources/app/Template/swimlane/table.php +++ b/sources/app/Template/swimlane/table.php @@ -1,6 +1,6 @@
    + data-save-position-url="url->href('SwimlaneController', 'move', array('project_id' => $project['id'])) ?>"> @@ -20,13 +20,13 @@
    • - url->link(t('Edit'), 'Swimlane', 'editDefault', array('project_id' => $project['id']), false, 'popover') ?> + url->link(t('Edit'), 'SwimlaneController', 'editDefault', array('project_id' => $project['id']), false, 'popover') ?>
    • - url->link(t('Disable'), 'Swimlane', 'disableDefault', array('project_id' => $project['id']), true) ?> + url->link(t('Disable'), 'SwimlaneController', 'disableDefault', array('project_id' => $project['id']), true) ?> - url->link(t('Enable'), 'Swimlane', 'enableDefault', array('project_id' => $project['id']), true) ?> + url->link(t('Enable'), 'SwimlaneController', 'enableDefault', array('project_id' => $project['id']), true) ?>
    @@ -45,7 +45,7 @@ text->e($swimlane['name']) ?> - '> + @@ -55,17 +55,17 @@
    • - url->link(t('Edit'), 'swimlane', 'edit', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?> + url->link(t('Edit'), 'SwimlaneController', 'edit', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?>
    • - url->link(t('Disable'), 'swimlane', 'disable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + url->link(t('Disable'), 'SwimlaneController', 'disable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> - url->link(t('Enable'), 'swimlane', 'enable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + url->link(t('Enable'), 'SwimlaneController', 'enable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?>
    • - url->link(t('Remove'), 'swimlane', 'confirm', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?> + url->link(t('Remove'), 'SwimlaneController', 'confirm', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), false, 'popover') ?>
    diff --git a/sources/app/Template/tag/create.php b/sources/app/Template/tag/create.php new file mode 100644 index 0000000..9b32bc4 --- /dev/null +++ b/sources/app/Template/tag/create.php @@ -0,0 +1,16 @@ + +
    + form->csrf() ?> + form->hidden('project_id', $values) ?> + + form->label(t('Name'), 'name') ?> + form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="255"')) ?> + +
    + + + url->link(t('cancel'), 'TagController', 'index', array(), false, 'close-popover') ?> +
    + diff --git a/sources/app/Template/tag/edit.php b/sources/app/Template/tag/edit.php new file mode 100644 index 0000000..f751ff4 --- /dev/null +++ b/sources/app/Template/tag/edit.php @@ -0,0 +1,17 @@ + +
    + form->csrf() ?> + form->hidden('id', $values) ?> + form->hidden('project_id', $values) ?> + + form->label(t('Name'), 'name') ?> + form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="255"')) ?> + +
    + + + url->link(t('cancel'), 'TagController', 'index', array(), false, 'close-popover') ?> +
    + diff --git a/sources/app/Template/tag/index.php b/sources/app/Template/tag/index.php new file mode 100644 index 0000000..2a495eb --- /dev/null +++ b/sources/app/Template/tag/index.php @@ -0,0 +1,31 @@ + + + +

    + +
    + + + + + + + + + + +
    text->e($tag['name']) ?> + + url->link(t('Remove'), 'TagController', 'confirm', array('tag_id' => $tag['id']), false, 'popover') ?> + + url->link(t('Edit'), 'TagController', 'edit', array('tag_id' => $tag['id']), false, 'popover') ?> +
    + diff --git a/sources/app/Template/tag/remove.php b/sources/app/Template/tag/remove.php new file mode 100644 index 0000000..46ea3f9 --- /dev/null +++ b/sources/app/Template/tag/remove.php @@ -0,0 +1,15 @@ + + +
    +

    + +

    + +
    + url->link(t('Yes'), 'TagController', 'remove', array('tag_id' => $tag['id']), true, 'btn btn-red popover-link') ?> + + url->link(t('cancel'), 'TagController', 'index', array(), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/task/analytics.php b/sources/app/Template/task/analytics.php index 54eac2b..db2d0ce 100644 --- a/sources/app/Template/task/analytics.php +++ b/sources/app/Template/task/analytics.php @@ -34,6 +34,3 @@
  • - -asset->js('assets/js/vendor/d3.v3.min.js') ?> -asset->js('assets/js/vendor/c3.min.js') ?> \ No newline at end of file diff --git a/sources/app/Template/task/color_picker.php b/sources/app/Template/task/color_picker.php deleted file mode 100644 index 0c62fa7..0000000 --- a/sources/app/Template/task/color_picker.php +++ /dev/null @@ -1,11 +0,0 @@ -
    - $color_name): ?> -
    -
    - -
    - -form->hidden('color_id', $values) ?> \ No newline at end of file diff --git a/sources/app/Template/task/description.php b/sources/app/Template/task/description.php index 9ffe858..f8e313d 100644 --- a/sources/app/Template/task/description.php +++ b/sources/app/Template/task/description.php @@ -4,29 +4,7 @@
    - - text->markdown( - $task['description'], - array( - 'controller' => 'task', - 'action' => 'show', - 'params' => array( - 'project_id' => $task['project_id'] - ) - ) - ) ?> - - text->markdown( - $task['description'], - array( - 'controller' => 'task', - 'action' => 'readonly', - 'params' => array( - 'token' => $project['token'] - ) - ) - ) ?> - + text->markdown($task['description'], isset($is_public) && $is_public) ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task/details.php b/sources/app/Template/task/details.php index 6093c15..695957f 100644 --- a/sources/app/Template/task/details.php +++ b/sources/app/Template/task/details.php @@ -1,144 +1,167 @@

    text->e($task['title']) ?>

    + hook->render('template:task:details:top', array('task' => $task)) ?> +
    -
    -
      -
    • - - - - - - +
      +
      +
        +
      • + + + + + + + + +
      • +
      • + +
      • + +
      • + text->e($task['reference']) ?> +
      • - - -
      • - -
      • - -
      • - text->e($task['reference']) ?> -
      • - - -
      • - text->e($task['score']) ?> -
      • - - -
      • - - url->link(t('Public link'), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?> -
      • - - -
      • - - url->link(t('Back to the board'), 'board', 'readonly', array('token' => $project['token'])) ?> -
      • - -
      • -
      -
      -
      -
        - -
      • - - text->e($task['category_name']) ?> -
      • - - -
      • - - text->e($task['swimlane_name']) ?> -
      • - -
      • - - text->e($task['column_title']) ?> -
      • -
      • - - -
      • -
      -
      -
      -
        -
      • - - - - text->e($task['assignee_name'] ?: $task['assignee_username']) ?> - - + +
      • + text->e($task['score']) ?> +
      • - - - -
      • - - text->e($task['creator_name'] ?: $task['creator_username']) ?> + +
      • + + url->link(t('Public link'), 'TaskViewController', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?>
      • - - -
      • - - dt->date($task['date_due']) ?> -
      • - - -
      • - - -
      • - - -
      • - - -
      • - -
      -
      -
      -
        -
      • - - dt->datetime($task['date_creation']) ?> -
      • -
      • - - dt->datetime($task['date_modification']) ?> -
      • - -
      • - - dt->datetime($task['date_completed']) ?> -
      • - - -
      • - - dt->datetime($task['date_started']) ?> -
      • - - -
      • - - dt->datetime($task['date_moved']) ?> -
      • - -
      + + +
    • + + url->link(t('Back to the board'), 'BoardViewController', 'readonly', array('token' => $project['token'])) ?> +
    • + +
    • + + hook->render('template:task:details:first-column', array('task' => $task)) ?> +
    +
    +
    +
      + +
    • + + text->e($task['category_name']) ?> +
    • + + +
    • + + text->e($task['swimlane_name']) ?> +
    • + +
    • + + text->e($task['column_title']) ?> +
    • +
    • + + +
    • + + hook->render('template:task:details:second-column', array('task' => $task)) ?> +
    +
    +
    +
      +
    • + + + + text->e($task['assignee_name'] ?: $task['assignee_username']) ?> + + + + +
    • + +
    • + + text->e($task['creator_name'] ?: $task['creator_username']) ?> +
    • + + +
    • + + dt->date($task['date_due']) ?> +
    • + + +
    • + + +
    • + + +
    • + + +
    • + + + hook->render('template:task:details:third-column', array('task' => $task)) ?> +
    +
    +
    +
      +
    • + + dt->datetime($task['date_creation']) ?> +
    • +
    • + + dt->datetime($task['date_modification']) ?> +
    • + +
    • + + dt->datetime($task['date_completed']) ?> +
    • + + +
    • + + dt->datetime($task['date_started']) ?> +
    • + + +
    • + + dt->datetime($task['date_moved']) ?> +
    • + + + hook->render('template:task:details:fourth-column', array('task' => $task)) ?> +
    +
    + +
    +
      + +
    • text->e($tag) ?>
    • + +
    +
    +
    - url->button('fa-play', t('Set start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + url->button('fa-play', t('Set start date'), 'TaskModificationController', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
    + + hook->render('template:task:details:bottom', array('task' => $task)) ?>
    diff --git a/sources/app/Template/task/dropdown.php b/sources/app/Template/task/dropdown.php index f98f517..95c7a88 100644 --- a/sources/app/Template/task/dropdown.php +++ b/sources/app/Template/task/dropdown.php @@ -1,60 +1,66 @@ diff --git a/sources/app/Template/task/layout.php b/sources/app/Template/task/layout.php index 52db5d1..7f6c291 100644 --- a/sources/app/Template/task/layout.php +++ b/sources/app/Template/task/layout.php @@ -1,12 +1,12 @@
    - projectHeader->render($project, 'Listing', 'show') ?> + projectHeader->render($project, 'TaskListController', 'show') ?> + hook->render('template:task:layout:top', array('task' => $task)) ?> -
    \ No newline at end of file + diff --git a/sources/app/Template/task/public.php b/sources/app/Template/task/public.php index 9478216..b8405ff 100644 --- a/sources/app/Template/task/public.php +++ b/sources/app/Template/task/public.php @@ -1,5 +1,10 @@
    - render('task/details', array('task' => $task, 'project' => $project, 'editable' => false)) ?> + render('task/details', array( + 'task' => $task, + 'tags' => $tags, + 'project' => $project, + 'editable' => false, + )) ?> render('task/description', array( 'task' => $task, diff --git a/sources/app/Template/task/remove.php b/sources/app/Template/task/remove.php deleted file mode 100644 index eb0809b..0000000 --- a/sources/app/Template/task/remove.php +++ /dev/null @@ -1,15 +0,0 @@ - - -
    -

    - text->e($task['title'])) ?> -

    - -
    - url->link(t('Yes'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> - - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> -
    -
    \ No newline at end of file diff --git a/sources/app/Template/task/show.php b/sources/app/Template/task/show.php index 8642294..8078671 100644 --- a/sources/app/Template/task/show.php +++ b/sources/app/Template/task/show.php @@ -2,8 +2,9 @@ render('task/details', array( 'task' => $task, + 'tags' => $tags, 'project' => $project, - 'editable' => $this->user->hasProjectAccess('taskmodification', 'edit', $project['id']), + 'editable' => $this->user->hasProjectAccess('TaskModificationController', 'edit', $project['id']), )) ?> hook->render('template:task:show:before-description', array('task' => $task, 'project' => $project)) ?> @@ -34,7 +35,7 @@ 'project' => $project, )) ?> -hook->render('template:task:show:before-attachements', array('task' => $task, 'project' => $project)) ?> +hook->render('template:task:show:before-attachments', array('task' => $task, 'project' => $project)) ?> render('task_file/show', array( 'task' => $task, 'files' => $files, @@ -46,7 +47,7 @@ 'task' => $task, 'comments' => $comments, 'project' => $project, - 'editable' => $this->user->hasProjectAccess('comment', 'edit', $project['id']), + 'editable' => $this->user->hasProjectAccess('CommentController', 'edit', $project['id']), )) ?> hook->render('template:task:show:bottom', array('task' => $task, 'project' => $project)) ?> diff --git a/sources/app/Template/task/sidebar.php b/sources/app/Template/task/sidebar.php index 773b28d..b44e6f0 100644 --- a/sources/app/Template/task/sidebar.php +++ b/sources/app/Template/task/sidebar.php @@ -1,98 +1,96 @@ diff --git a/sources/app/Template/task/time_tracking_details.php b/sources/app/Template/task/time_tracking_details.php index c51b8f5..1a17952 100644 --- a/sources/app/Template/task/time_tracking_details.php +++ b/sources/app/Template/task/time_tracking_details.php @@ -14,11 +14,11 @@ order(t('Subtask'), 'subtask_title') ?> order(t('Start'), 'start') ?> order(t('End'), 'end') ?> - order(t('Time spent'), \Kanboard\Model\SubtaskTimeTracking::TABLE.'.time_spent') ?> + order(t('Time spent'), \Kanboard\Model\SubtaskTimeTrackingModel::TABLE.'.time_spent') ?> getCollection() as $record): ?> - url->link($this->text->e($record['user_fullname'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?> + url->link($this->text->e($record['user_fullname'] ?: $record['username']), 'UserViewController', 'show', array('user_id' => $record['user_id'])) ?> dt->datetime($record['start']) ?> dt->datetime($record['end']) ?> @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/sources/app/Template/task/transitions.php b/sources/app/Template/task/transitions.php index 8304017..9e04c4e 100644 --- a/sources/app/Template/task/transitions.php +++ b/sources/app/Template/task/transitions.php @@ -22,9 +22,9 @@ dt->datetime($transition['date']) ?> text->e($transition['src_column']) ?> text->e($transition['dst_column']) ?> - url->link($this->text->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?> + url->link($this->text->e($transition['name'] ?: $transition['username']), 'UserViewController', 'show', array('user_id' => $transition['user_id'])) ?> dt->duration($transition['time_spent']) ?> - \ No newline at end of file + diff --git a/sources/app/Template/task_bulk/show.php b/sources/app/Template/task_bulk/show.php new file mode 100644 index 0000000..e9b138d --- /dev/null +++ b/sources/app/Template/task_bulk/show.php @@ -0,0 +1,24 @@ + + +
    + form->csrf() ?> + form->hidden('column_id', $values) ?> + form->hidden('swimlane_id', $values) ?> + form->hidden('project_id', $values) ?> + + task->selectColor($values) ?> + task->selectAssignee($users_list, $values, $errors) ?> + task->selectCategory($categories_list, $values, $errors) ?> + + form->label(t('Tasks'), 'tasks') ?> + form->textarea('tasks', $values, $errors, array('placeholder="'.t('My task title').'"')) ?> +

    + +
    + + url->link(t('cancel'), 'BoardViewController', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> +
    +
    + diff --git a/sources/app/Template/task_creation/form.php b/sources/app/Template/task_creation/form.php deleted file mode 100644 index 9bfd839..0000000 --- a/sources/app/Template/task_creation/form.php +++ /dev/null @@ -1,53 +0,0 @@ - - -
    - - form->csrf() ?> - -
    - form->label(t('Title'), 'title') ?> - form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?> - - form->label(t('Description'), 'description') ?> - form->textarea( - 'description', - $values, - $errors, - array( - 'placeholder="'.t('Leave a description').'"', - 'tabindex="2"', - 'data-mention-search-url="'.$this->url->href('UserHelper', 'mention', array('project_id' => $values['project_id'])).'"' - ), - 'markdown-editor' - ) ?> - - render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?> - - - form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> - - - hook->render('template:task:form:left-column', array('values'=>$values, 'errors'=>$errors)) ?> -
    - -
    - form->hidden('project_id', $values) ?> - task->selectAssignee($users_list, $values, $errors) ?> - task->selectCategory($categories_list, $values, $errors) ?> - task->selectSwimlane($swimlanes_list, $values, $errors) ?> - task->selectColumn($columns_list, $values, $errors) ?> - task->selectPriority($project, $values) ?> - task->selectScore($values, $errors) ?> - task->selectTimeEstimated($values, $errors) ?> - task->selectDueDate($values, $errors) ?> - - hook->render('template:task:form:right-column', array('values'=>$values, 'errors'=>$errors)) ?> -
    - -
    - - url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> -
    -
    diff --git a/sources/app/Template/task_creation/show.php b/sources/app/Template/task_creation/show.php new file mode 100644 index 0000000..57e77f3 --- /dev/null +++ b/sources/app/Template/task_creation/show.php @@ -0,0 +1,49 @@ + + +
    + form->csrf() ?> + +
    +
    + task->selectTitle($values, $errors) ?> + task->selectDescription($values, $errors) ?> + task->selectTags($project) ?> + + + form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> + + + hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?> +
    + +
    + form->hidden('project_id', $values) ?> + task->selectColor($values) ?> + task->selectAssignee($users_list, $values, $errors) ?> + task->selectCategory($categories_list, $values, $errors) ?> + task->selectSwimlane($swimlanes_list, $values, $errors) ?> + task->selectColumn($columns_list, $values, $errors) ?> + task->selectPriority($project, $values) ?> + task->selectScore($values, $errors) ?> + task->selectReference($values, $errors) ?> + + hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> +
    + +
    + task->selectTimeEstimated($values, $errors) ?> + task->selectTimeSpent($values, $errors) ?> + task->selectStartDate($values, $errors) ?> + task->selectDueDate($values, $errors) ?> + + hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?> +
    +
    + +
    + + url->link(t('cancel'), 'BoardViewController', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/task_duplication/copy.php b/sources/app/Template/task_duplication/copy.php index b7337a1..58b4d83 100644 --- a/sources/app/Template/task_duplication/copy.php +++ b/sources/app/Template/task_duplication/copy.php @@ -6,7 +6,7 @@

    -
    + form->csrf() ?> form->hidden('id', $values) ?> @@ -17,7 +17,7 @@ $projects_list, $values, array(), - array('data-redirect="'.$this->url->href('taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'dst_project_id' => 'PROJECT_ID')).'"'), + array('data-redirect="'.$this->url->href('TaskDuplicationController', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'dst_project_id' => 'PROJECT_ID')).'"'), 'task-reload-project-destination' ) ?> @@ -41,8 +41,8 @@
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_duplication/duplicate.php b/sources/app/Template/task_duplication/duplicate.php index 376f6b3..c0baf94 100644 --- a/sources/app/Template/task_duplication/duplicate.php +++ b/sources/app/Template/task_duplication/duplicate.php @@ -8,8 +8,8 @@

    - url->link(t('Yes'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + url->link(t('Yes'), 'TaskDuplicationController', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_duplication/move.php b/sources/app/Template/task_duplication/move.php index beebf9e..8f01c4b 100644 --- a/sources/app/Template/task_duplication/move.php +++ b/sources/app/Template/task_duplication/move.php @@ -6,7 +6,7 @@

    -
    + form->csrf() ?> form->hidden('id', $values) ?> @@ -17,7 +17,7 @@ $projects_list, $values, array(), - array('data-redirect="'.$this->url->href('taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'dst_project_id' => 'PROJECT_ID')).'"'), + array('data-redirect="'.$this->url->href('TaskDuplicationController', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'dst_project_id' => 'PROJECT_ID')).'"'), 'task-reload-project-destination' ) ?> @@ -41,8 +41,8 @@
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_external_link/create.php b/sources/app/Template/task_external_link/create.php index 5d49eef..beddfc9 100644 --- a/sources/app/Template/task_external_link/create.php +++ b/sources/app/Template/task_external_link/create.php @@ -2,12 +2,12 @@

    -
    + render('task_external_link/form', array('task' => $task, 'dependencies' => $dependencies, 'values' => $values, 'errors' => $errors)) ?>
    - url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskExternalLinkController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/task_external_link/edit.php b/sources/app/Template/task_external_link/edit.php index dcbc263..917a28b 100644 --- a/sources/app/Template/task_external_link/edit.php +++ b/sources/app/Template/task_external_link/edit.php @@ -2,12 +2,12 @@

    -
    + render('task_external_link/form', array('task' => $task, 'dependencies' => $dependencies, 'values' => $values, 'errors' => $errors)) ?>
    - url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskExternalLinkController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/task_external_link/find.php b/sources/app/Template/task_external_link/find.php index 3977a66..a88b29c 100644 --- a/sources/app/Template/task_external_link/find.php +++ b/sources/app/Template/task_external_link/find.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> form->hidden('task_id', array('task_id' => $task['id'])) ?> @@ -23,6 +23,6 @@
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/task_external_link/remove.php b/sources/app/Template/task_external_link/remove.php index 0153525..2a888a6 100644 --- a/sources/app/Template/task_external_link/remove.php +++ b/sources/app/Template/task_external_link/remove.php @@ -8,8 +8,8 @@

    - url->link(t('Yes'), 'TaskExternalLink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'TaskExternalLinkController', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'TaskExternalLink', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskExternalLinkController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_external_link/table.php b/sources/app/Template/task_external_link/table.php index 1f32f4d..56ef036 100644 --- a/sources/app/Template/task_external_link/table.php +++ b/sources/app/Template/task_external_link/table.php @@ -6,7 +6,7 @@ - user->hasProjectAccess('TaskExternalLink', 'edit', $task['project_id'])): ?> + user->hasProjectAccess('TaskExternalLinkController', 'edit', $task['project_id'])): ?> @@ -27,13 +27,13 @@ dt->date($link['date_creation']) ?> - user->hasProjectAccess('TaskExternalLink', 'edit', $task['project_id'])): ?> + user->hasProjectAccess('TaskExternalLinkController', 'edit', $task['project_id'])): ?> diff --git a/sources/app/Template/task_file/create.php b/sources/app/Template/task_file/create.php index f03ce8d..e05cf82 100644 --- a/sources/app/Template/task_file/create.php +++ b/sources/app/Template/task_file/create.php @@ -4,7 +4,7 @@ @@ -18,7 +18,7 @@
    + data-url="url->href('TaskFileController', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
    @@ -27,7 +27,7 @@
    - + - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    diff --git a/sources/app/Template/task_file/files.php b/sources/app/Template/task_file/files.php index 5603014..7ca59b1 100644 --- a/sources/app/Template/task_file/files.php +++ b/sources/app/Template/task_file/files.php @@ -16,17 +16,17 @@ file->getPreviewType($file['name']) !== null): ?>
  • - url->link(t('View file'), 'FileViewer', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + url->link(t('View file'), 'FileViewerController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
  • - url->link(t('Download'), 'FileViewer', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + url->link(t('Download'), 'FileViewerController', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?>
  • - user->hasProjectAccess('TaskFile', 'remove', $task['project_id'])): ?> + user->hasProjectAccess('TaskFileController', 'remove', $task['project_id'])): ?>
  • - url->link(t('Remove'), 'TaskFile', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + url->link(t('Remove'), 'TaskFileController', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?>
  • @@ -44,4 +44,4 @@ - \ No newline at end of file + diff --git a/sources/app/Template/task_file/images.php b/sources/app/Template/task_file/images.php index 55c098c..81c3315 100644 --- a/sources/app/Template/task_file/images.php +++ b/sources/app/Template/task_file/images.php @@ -2,7 +2,7 @@
    - <?= $this->text->e($file['name']) ?> + <?= $this->text->e($file['name']) ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_file/remove.php b/sources/app/Template/task_file/remove.php index fe601f6..42894f0 100644 --- a/sources/app/Template/task_file/remove.php +++ b/sources/app/Template/task_file/remove.php @@ -8,8 +8,8 @@

    - url->link(t('Yes'), 'TaskFile', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'TaskFileController', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    -
    \ No newline at end of file +
    diff --git a/sources/app/Template/task_file/screenshot.php b/sources/app/Template/task_file/screenshot.php index 2880478..6300159 100644 --- a/sources/app/Template/task_file/screenshot.php +++ b/sources/app/Template/task_file/screenshot.php @@ -6,14 +6,14 @@

    -
    + form->csrf() ?>
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    -

    \ No newline at end of file +

    diff --git a/sources/app/Template/gantt/project.php b/sources/app/Template/task_gantt/show.php similarity index 62% rename from sources/app/Template/gantt/project.php rename to sources/app/Template/task_gantt/show.php index e6c8592..c5d338f 100644 --- a/sources/app/Template/gantt/project.php +++ b/sources/app/Template/task_gantt/show.php @@ -1,18 +1,18 @@
    - projectHeader->render($project, 'Gantt', 'project') ?> + projectHeader->render($project, 'TaskGanttController', 'show') ?> @@ -21,7 +21,7 @@

    -
    \ No newline at end of file +
    diff --git a/sources/app/Template/task_gantt_creation/show.php b/sources/app/Template/task_gantt_creation/show.php new file mode 100644 index 0000000..7521d80 --- /dev/null +++ b/sources/app/Template/task_gantt_creation/show.php @@ -0,0 +1,46 @@ + +
    + form->csrf() ?> + form->hidden('project_id', $values) ?> + form->hidden('column_id', $values) ?> + form->hidden('position', $values) ?> + +
    +
    + task->selectTitle($values, $errors) ?> + task->selectDescription($values, $errors) ?> + task->selectTags($project) ?> + + hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?> +
    + +
    + task->selectColor($values) ?> + task->selectAssignee($users_list, $values, $errors) ?> + task->selectCategory($categories_list, $values, $errors) ?> + task->selectSwimlane($swimlanes_list, $values, $errors) ?> + task->selectPriority($project, $values) ?> + task->selectScore($values, $errors) ?> + task->selectReference($values, $errors) ?> + + hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> +
    + +
    + task->selectTimeEstimated($values, $errors) ?> + task->selectTimeSpent($values, $errors) ?> + task->selectStartDate($values, $errors) ?> + task->selectDueDate($values, $errors) ?> + + hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?> +
    +
    + +
    + + + url->link(t('cancel'), 'TaskGanttController', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/task_import/step1.php b/sources/app/Template/task_import/show.php similarity index 82% rename from sources/app/Template/task_import/step1.php rename to sources/app/Template/task_import/show.php index abb4350..cc6a7b3 100644 --- a/sources/app/Template/task_import/step1.php +++ b/sources/app/Template/task_import/show.php @@ -1,7 +1,7 @@ -
    + form->csrf() ?> form->label(t('Delimiter'), 'delimiter') ?> @@ -31,4 +31,4 @@
  • -

    url->link(t('Download CSV template'), 'taskImport', 'template', array('project_id' => $project['id'])) ?>

    \ No newline at end of file +

    url->link(t('Download CSV template'), 'TaskImportController', 'template', array('project_id' => $project['id'])) ?>

    diff --git a/sources/app/Template/task_import/sidebar.php b/sources/app/Template/task_import/sidebar.php new file mode 100644 index 0000000..4cd92af --- /dev/null +++ b/sources/app/Template/task_import/sidebar.php @@ -0,0 +1,9 @@ + diff --git a/sources/app/Template/task_internal_link/create.php b/sources/app/Template/task_internal_link/create.php index 94dcdd6..fed2960 100644 --- a/sources/app/Template/task_internal_link/create.php +++ b/sources/app/Template/task_internal_link/create.php @@ -2,7 +2,7 @@

    - + form->csrf() ?> form->hidden('task_id', array('task_id' => $task['id'])) ?> @@ -21,13 +21,13 @@ 'placeholder="'.t('Start to type task title...').'"', 'title="'.t('Start to type task title...').'"', 'data-dst-field="opposite_task_id"', - 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', + 'data-search-url="'.$this->url->href('TaskAjaxController', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', ), 'autocomplete') ?>
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/task_internal_link/edit.php b/sources/app/Template/task_internal_link/edit.php index 03622df..f4df57b 100644 --- a/sources/app/Template/task_internal_link/edit.php +++ b/sources/app/Template/task_internal_link/edit.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?> form->hidden('id', $values) ?> @@ -22,13 +22,13 @@ 'placeholder="'.t('Start to type task title...').'"', 'title="'.t('Start to type task title...').'"', 'data-dst-field="opposite_task_id"', - 'data-search-url="'.$this->url->href('TaskHelper', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', + 'data-search-url="'.$this->url->href('TaskAjaxController', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', ), 'autocomplete') ?>
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    -
    \ No newline at end of file + diff --git a/sources/app/Template/task_internal_link/remove.php b/sources/app/Template/task_internal_link/remove.php index 82156ec..966ad11 100644 --- a/sources/app/Template/task_internal_link/remove.php +++ b/sources/app/Template/task_internal_link/remove.php @@ -8,8 +8,8 @@

    - url->link(t('Yes'), 'TaskInternalLink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'TaskInternalLinkController', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_internal_link/table.php b/sources/app/Template/task_internal_link/table.php index 8f25b29..424d479 100644 --- a/sources/app/Template/task_internal_link/table.php +++ b/sources/app/Template/task_internal_link/table.php @@ -24,7 +24,7 @@ url->link( $this->text->e('#'.$link['task_id'].' '.$link['title']), - 'task', + 'TaskViewController', 'readonly', array('task_id' => $link['task_id'], 'token' => $project['token']), false, @@ -33,7 +33,7 @@ url->link( $this->text->e('#'.$link['task_id'].' '.$link['title']), - 'task', + 'TaskViewController', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), false, @@ -52,7 +52,7 @@ - url->link($this->text->e($link['task_assignee_name'] ?: $link['task_assignee_username']), 'user', 'show', array('user_id' => $link['task_assignee_id'])) ?> + url->link($this->text->e($link['task_assignee_name'] ?: $link['task_assignee_username']), 'UserViewController', 'show', array('user_id' => $link['task_assignee_id'])) ?> text->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?> @@ -72,8 +72,8 @@ @@ -82,4 +82,4 @@ - \ No newline at end of file + diff --git a/sources/app/Template/listing/show.php b/sources/app/Template/task_list/show.php similarity index 87% rename from sources/app/Template/listing/show.php rename to sources/app/Template/task_list/show.php index 98b9528..bb95b6a 100644 --- a/sources/app/Template/listing/show.php +++ b/sources/app/Template/task_list/show.php @@ -1,5 +1,5 @@
    - projectHeader->render($project, 'Listing', 'show') ?> + projectHeader->render($project, 'TaskListController', 'show') ?> isEmpty()): ?>

    @@ -18,7 +18,7 @@ getCollection() as $task): ?> - user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?> + user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])): ?> render('task/dropdown', array('task' => $task)) ?> # @@ -34,7 +34,7 @@ text->e($task['category_name']) ?> - url->link($this->text->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> @@ -47,7 +47,7 @@ dt->date($task['date_due']) ?> - + @@ -59,4 +59,4 @@ -
    \ No newline at end of file + diff --git a/sources/app/Template/task_modification/edit_description.php b/sources/app/Template/task_modification/edit_description.php deleted file mode 100644 index 8c149f4..0000000 --- a/sources/app/Template/task_modification/edit_description.php +++ /dev/null @@ -1,27 +0,0 @@ - - -
    - - form->csrf() ?> - form->hidden('id', $values) ?> - - form->textarea( - 'description', - $values, - $errors, - array( - 'autofocus', - 'placeholder="'.t('Leave a description').'"', - 'data-mention-search-url="'.$this->url->href('UserHelper', 'mention', array('project_id' => $task['project_id'])).'"' - ), - 'markdown-editor' - ) ?> - -
    - - - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> -
    -
    diff --git a/sources/app/Template/task_modification/edit_task.php b/sources/app/Template/task_modification/edit_task.php deleted file mode 100644 index b5891c1..0000000 --- a/sources/app/Template/task_modification/edit_task.php +++ /dev/null @@ -1,35 +0,0 @@ - -
    - - form->csrf() ?> - form->hidden('id', $values) ?> - form->hidden('project_id', $values) ?> - -
    - form->label(t('Title'), 'title') ?> - form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"')) ?> - task->selectAssignee($users_list, $values, $errors) ?> - task->selectCategory($categories_list, $values, $errors) ?> - task->selectPriority($project, $values) ?> - task->selectScore($values, $errors) ?> -
    - -
    - task->selectTimeEstimated($values, $errors) ?> - task->selectTimeSpent($values, $errors) ?> - task->selectStartDate($values, $errors) ?> - task->selectDueDate($values, $errors) ?> -
    - -
    - render('task/color_picker', array('colors_list' => $colors_list, 'values' => $values)) ?> -
    - -
    - - - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> -
    -
    \ No newline at end of file diff --git a/sources/app/Template/task_modification/show.php b/sources/app/Template/task_modification/show.php new file mode 100644 index 0000000..cc38582 --- /dev/null +++ b/sources/app/Template/task_modification/show.php @@ -0,0 +1,44 @@ + +
    + form->csrf() ?> + form->hidden('id', $values) ?> + form->hidden('project_id', $values) ?> + +
    +
    + task->selectTitle($values, $errors) ?> + task->selectDescription($values, $errors) ?> + task->selectTags($project, $tags) ?> + + hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?> +
    + +
    + task->selectColor($values) ?> + task->selectAssignee($users_list, $values, $errors) ?> + task->selectCategory($categories_list, $values, $errors) ?> + task->selectPriority($project, $values) ?> + task->selectScore($values, $errors) ?> + task->selectReference($values, $errors) ?> + + hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?> +
    + +
    + task->selectTimeEstimated($values, $errors) ?> + task->selectTimeSpent($values, $errors) ?> + task->selectStartDate($values, $errors) ?> + task->selectDueDate($values, $errors) ?> + + hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?> +
    +
    + +
    + + + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/task_recurrence/edit.php b/sources/app/Template/task_recurrence/edit.php index 0f5d611..09d1482 100644 --- a/sources/app/Template/task_recurrence/edit.php +++ b/sources/app/Template/task_recurrence/edit.php @@ -2,7 +2,7 @@

    - +
    render('task_recurrence/info', array( 'task' => $task, @@ -13,9 +13,9 @@
    - + -
    + form->csrf() ?> @@ -40,8 +40,8 @@
    - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_recurrence/info.php b/sources/app/Template/task_recurrence/info.php index 1a6574d..04d58c7 100644 --- a/sources/app/Template/task_recurrence/info.php +++ b/sources/app/Template/task_recurrence/info.php @@ -1,7 +1,7 @@
      - +
    • - +
      • @@ -24,14 +24,14 @@
      • - url->link('#'.$task['recurrence_parent'], 'task', 'show', array('task_id' => $task['recurrence_parent'], 'project_id' => $task['project_id'])) ?> + url->link('#'.$task['recurrence_parent'], 'TaskViewController', 'show', array('task_id' => $task['recurrence_parent'], 'project_id' => $task['project_id'])) ?>
      • - url->link('#'.$task['recurrence_child'], 'task', 'show', array('task_id' => $task['recurrence_child'], 'project_id' => $task['project_id'])) ?> + url->link('#'.$task['recurrence_child'], 'TaskViewController', 'show', array('task_id' => $task['recurrence_child'], 'project_id' => $task['project_id'])) ?>
      • -
      \ No newline at end of file +
    diff --git a/sources/app/Template/task_status/close.php b/sources/app/Template/task_status/close.php index 7d20054..2d7b0ce 100644 --- a/sources/app/Template/task_status/close.php +++ b/sources/app/Template/task_status/close.php @@ -8,8 +8,8 @@

    - url->link(t('Yes'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red popover-link') ?> + url->link(t('Yes'), 'TaskStatusController', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red popover-link') ?> - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_status/open.php b/sources/app/Template/task_status/open.php index 5d19bfb..242b5db 100644 --- a/sources/app/Template/task_status/open.php +++ b/sources/app/Template/task_status/open.php @@ -8,8 +8,8 @@

    - url->link(t('Yes'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red popover-link') ?> + url->link(t('Yes'), 'TaskStatusController', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red popover-link') ?> - url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/task_suppression/remove.php b/sources/app/Template/task_suppression/remove.php new file mode 100644 index 0000000..5d0f772 --- /dev/null +++ b/sources/app/Template/task_suppression/remove.php @@ -0,0 +1,15 @@ + + +
    +

    + text->e($task['title'])) ?> +

    + +
    + url->link(t('Yes'), 'TaskSuppressionController', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => $redirect), true, 'btn btn-red popover-link') ?> + + url->link(t('cancel'), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/twofactor/check.php b/sources/app/Template/twofactor/check.php index b0cb482..06801d5 100644 --- a/sources/app/Template/twofactor/check.php +++ b/sources/app/Template/twofactor/check.php @@ -1,4 +1,4 @@ -
    + form->csrf() ?> form->label(t('Code'), 'code') ?> @@ -7,4 +7,4 @@
    -
    \ No newline at end of file + diff --git a/sources/app/Template/twofactor/disable.php b/sources/app/Template/twofactor/disable.php index 36be4ef..bc41918 100644 --- a/sources/app/Template/twofactor/disable.php +++ b/sources/app/Template/twofactor/disable.php @@ -8,7 +8,7 @@

    - url->link(t('Yes'), 'twofactor', 'disable', array('user_id' => $user['id'], 'disable' => 'yes'), true, 'btn btn-red') ?> - url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + url->link(t('Yes'), 'TwoFactorController', 'disable', array('user_id' => $user['id'], 'disable' => 'yes'), true, 'btn btn-red') ?> + url->link(t('cancel'), 'UserViewController', 'show', array('user_id' => $user['id'])) ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/twofactor/index.php b/sources/app/Template/twofactor/index.php index 6de3651..1ed414e 100644 --- a/sources/app/Template/twofactor/index.php +++ b/sources/app/Template/twofactor/index.php @@ -2,7 +2,7 @@

    -
    + form->csrf() ?>

    text->e($provider) ?>

    diff --git a/sources/app/Template/twofactor/show.php b/sources/app/Template/twofactor/show.php index 59897e2..0aeef42 100644 --- a/sources/app/Template/twofactor/show.php +++ b/sources/app/Template/twofactor/show.php @@ -19,7 +19,7 @@

    - + form->csrf() ?> form->label(t('Code'), 'code') ?> @@ -28,4 +28,4 @@
    - \ No newline at end of file + diff --git a/sources/app/Template/user/dropdown.php b/sources/app/Template/user/dropdown.php deleted file mode 100644 index b74ed6e..0000000 --- a/sources/app/Template/user/dropdown.php +++ /dev/null @@ -1,27 +0,0 @@ - \ No newline at end of file diff --git a/sources/app/Template/user/layout.php b/sources/app/Template/user/layout.php deleted file mode 100644 index 3a0a5ba..0000000 --- a/sources/app/Template/user/layout.php +++ /dev/null @@ -1,19 +0,0 @@ -
    - - -
    \ No newline at end of file diff --git a/sources/app/Template/user/share.php b/sources/app/Template/user/share.php deleted file mode 100644 index 56dc867..0000000 --- a/sources/app/Template/user/share.php +++ /dev/null @@ -1,18 +0,0 @@ - - - - -
    -
      -
    • url->link(t('RSS feed'), 'feed', 'user', array('token' => $user['token']), false, '', '', true) ?>
    • -
    • url->link(t('iCal feed'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?>
    • -
    -
    - - url->link(t('Disable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> - - - url->link(t('Enable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> - diff --git a/sources/app/Template/user/sidebar.php b/sources/app/Template/user/sidebar.php deleted file mode 100644 index 5ea2e35..0000000 --- a/sources/app/Template/user/sidebar.php +++ /dev/null @@ -1,83 +0,0 @@ - \ No newline at end of file diff --git a/sources/app/Template/user/create_local.php b/sources/app/Template/user_creation/local.php similarity index 69% rename from sources/app/Template/user/create_local.php rename to sources/app/Template/user_creation/local.php index 7257456..059a011 100644 --- a/sources/app/Template/user/create_local.php +++ b/sources/app/Template/user_creation/local.php @@ -1,15 +1,10 @@ -
    - -
    -
    - - form->csrf() ?> + + + form->csrf() ?> +
    form->label(t('Username'), 'username') ?> form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> @@ -42,12 +37,11 @@ form->checkbox('notifications_enabled', t('Enable email notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?>
    +
    -
    - - - url->link(t('cancel'), 'user', 'index') ?> -
    -
    -
    -
    +
    + + + url->link(t('cancel'), 'UserListController', 'show', array(), false, 'close-popover') ?> +
    + diff --git a/sources/app/Template/user/create_remote.php b/sources/app/Template/user_creation/remote.php similarity index 59% rename from sources/app/Template/user/create_remote.php rename to sources/app/Template/user_creation/remote.php index 05acbba..41d0d3c 100644 --- a/sources/app/Template/user/create_remote.php +++ b/sources/app/Template/user_creation/remote.php @@ -1,19 +1,15 @@ -
    - -
    - - form->csrf() ?> - form->hidden('is_ldap_user', array('is_ldap_user' => 1)) ?> + + + form->csrf() ?> + form->hidden('is_ldap_user', array('is_ldap_user' => 1)) ?> +
    form->label(t('Username'), 'username') ?> form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> - + form->label(t('Name'), 'name') ?> form->text('name', $values, $errors) ?> @@ -39,17 +35,17 @@ form->checkbox('notifications_enabled', t('Enable email notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?> form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?>
    - -
    - - - url->link(t('cancel'), 'user', 'index') ?> -
    - -
    -
      -
    • -
    • -
    -
    \ No newline at end of file + +
    + + + url->link(t('cancel'), 'UserListController', 'show', array(), false, 'close-popover') ?> +
    + +
    +
      +
    • +
    • +
    +
    diff --git a/sources/app/Template/user/authentication.php b/sources/app/Template/user_credential/authentication.php similarity index 80% rename from sources/app/Template/user/authentication.php rename to sources/app/Template/user_credential/authentication.php index 6cfd4e5..fbe2e91 100644 --- a/sources/app/Template/user/authentication.php +++ b/sources/app/Template/user_credential/authentication.php @@ -1,8 +1,7 @@ -
    - + form->csrf() ?> form->hidden('id', $values) ?> @@ -16,7 +15,7 @@
    - url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + url->link(t('cancel'), 'UserViewController', 'show', array('user_id' => $user['id'])) ?>
    @@ -25,4 +24,4 @@
  • -
    \ No newline at end of file + diff --git a/sources/app/Template/user/password.php b/sources/app/Template/user_credential/password.php similarity index 57% rename from sources/app/Template/user/password.php rename to sources/app/Template/user_credential/password.php index ea6e997..5a6e440 100644 --- a/sources/app/Template/user/password.php +++ b/sources/app/Template/user_credential/password.php @@ -2,15 +2,12 @@

    -
    - + form->hidden('id', $values) ?> form->csrf() ?> -
    - form->label(t('Current password for the user "%s"', $this->user->getFullname()), 'current_password') ?> - form->password('current_password', $values, $errors) ?> -
    + form->label(t('Current password for the user "%s"', $this->user->getFullname()), 'current_password') ?> + form->password('current_password', $values, $errors) ?> form->label(t('New password for the user "%s"', $this->user->getFullname($user)), 'password') ?> form->password('password', $values, $errors) ?> @@ -21,6 +18,6 @@
    - url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + url->link(t('cancel'), 'UserViewController', 'show', array('user_id' => $user['id'])) ?>
    diff --git a/sources/app/Template/user_import/show.php b/sources/app/Template/user_import/show.php new file mode 100644 index 0000000..663f107 --- /dev/null +++ b/sources/app/Template/user_import/show.php @@ -0,0 +1,41 @@ + + +
    +
      +
    • +
    • +
    • +
    • +
    • +
    • +
    +
    + +
    + form->csrf() ?> + + form->label(t('Delimiter'), 'delimiter') ?> + form->select('delimiter', $delimiters, $values) ?> + + form->label(t('Enclosure'), 'enclosure') ?> + form->select('enclosure', $enclosures, $values) ?> + + form->label(t('CSV File'), 'file') ?> + form->file('file', $errors) ?> + +

    text->bytes($max_size) : $max_size ?>

    + +
    + + + url->link(t('cancel'), 'UserListController', 'show', array(), false, 'close-popover') ?> +
    +
    diff --git a/sources/app/Template/user_import/step1.php b/sources/app/Template/user_import/step1.php deleted file mode 100644 index 592587a..0000000 --- a/sources/app/Template/user_import/step1.php +++ /dev/null @@ -1,46 +0,0 @@ -
    - - -
    - form->csrf() ?> - - form->label(t('Delimiter'), 'delimiter') ?> - form->select('delimiter', $delimiters, $values) ?> - - form->label(t('Enclosure'), 'enclosure') ?> - form->select('enclosure', $enclosures, $values) ?> - - form->label(t('CSV File'), 'file') ?> - form->file('file', $errors) ?> - -

    text->bytes($max_size) : $max_size ?>

    - -
    - -
    -
    - -
    -
      -
    • -
    • -
    • -
    • -
    • -
    • -
    -
    -

    url->link(t('Download CSV template'), 'userImport', 'template') ?>

    -
    \ No newline at end of file diff --git a/sources/app/Template/user_list/dropdown.php b/sources/app/Template/user_list/dropdown.php new file mode 100644 index 0000000..9e90c23 --- /dev/null +++ b/sources/app/Template/user_list/dropdown.php @@ -0,0 +1,27 @@ + diff --git a/sources/app/Template/user/index.php b/sources/app/Template/user_list/show.php similarity index 76% rename from sources/app/Template/user/index.php rename to sources/app/Template/user_list/show.php index 364fd96..b2bd937 100644 --- a/sources/app/Template/user/index.php +++ b/sources/app/Template/user_list/show.php @@ -1,11 +1,11 @@
    @@ -14,6 +14,7 @@ + @@ -26,8 +27,10 @@ getCollection() as $user): ?> + diff --git a/sources/app/Template/user/edit.php b/sources/app/Template/user_modification/show.php similarity index 82% rename from sources/app/Template/user/edit.php rename to sources/app/Template/user_modification/show.php index 7b51eb7..396d550 100644 --- a/sources/app/Template/user/edit.php +++ b/sources/app/Template/user_modification/show.php @@ -1,7 +1,7 @@ - +form->csrf() ?> @@ -30,6 +30,6 @@
    - url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + url->link(t('cancel'), 'UserViewController', 'show', array('user_id' => $user['id'])) ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/user_status/disable.php b/sources/app/Template/user_status/disable.php index 90d8c75..d30b0c2 100644 --- a/sources/app/Template/user_status/disable.php +++ b/sources/app/Template/user_status/disable.php @@ -6,8 +6,8 @@

    - url->link(t('Yes'), 'UserStatus', 'disable', array('user_id' => $user['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'UserStatusController', 'disable', array('user_id' => $user['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?> + url->link(t('cancel'), 'UserListController', 'show', array(), false, 'close-popover') ?>
    diff --git a/sources/app/Template/user_status/enable.php b/sources/app/Template/user_status/enable.php index cd3d494..29d25ee 100644 --- a/sources/app/Template/user_status/enable.php +++ b/sources/app/Template/user_status/enable.php @@ -6,8 +6,8 @@

    - url->link(t('Yes'), 'UserStatus', 'enable', array('user_id' => $user['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'UserStatusController', 'enable', array('user_id' => $user['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?> + url->link(t('cancel'), 'UserListController', 'show', array(), false, 'close-popover') ?>
    diff --git a/sources/app/Template/user_status/remove.php b/sources/app/Template/user_status/remove.php index cd5c09a..2b8f2df 100644 --- a/sources/app/Template/user_status/remove.php +++ b/sources/app/Template/user_status/remove.php @@ -6,8 +6,8 @@

    - url->link(t('Yes'), 'UserStatus', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?> + url->link(t('Yes'), 'UserStatusController', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?> - url->link(t('cancel'), 'user', 'index', array(), false, 'close-popover') ?> + url->link(t('cancel'), 'UserListController', 'show', array(), false, 'close-popover') ?>
    diff --git a/sources/app/Template/user/external.php b/sources/app/Template/user_view/external.php similarity index 100% rename from sources/app/Template/user/external.php rename to sources/app/Template/user_view/external.php diff --git a/sources/app/Template/user/integrations.php b/sources/app/Template/user_view/integrations.php similarity index 72% rename from sources/app/Template/user/integrations.php rename to sources/app/Template/user_view/integrations.php index ef9d8e7..4a23734 100644 --- a/sources/app/Template/user/integrations.php +++ b/sources/app/Template/user_view/integrations.php @@ -2,7 +2,7 @@

    - +form->csrf() ?> hook->render('template:user:integrations', array('values' => $values)) ?> diff --git a/sources/app/Template/user/last.php b/sources/app/Template/user_view/last.php similarity index 100% rename from sources/app/Template/user/last.php rename to sources/app/Template/user_view/last.php diff --git a/sources/app/Template/user_view/layout.php b/sources/app/Template/user_view/layout.php new file mode 100644 index 0000000..c3604b9 --- /dev/null +++ b/sources/app/Template/user_view/layout.php @@ -0,0 +1,19 @@ +
    + + +
    diff --git a/sources/app/Template/user/notifications.php b/sources/app/Template/user_view/notifications.php similarity index 74% rename from sources/app/Template/user/notifications.php rename to sources/app/Template/user_view/notifications.php index 6e1a000..84ca128 100644 --- a/sources/app/Template/user/notifications.php +++ b/sources/app/Template/user_view/notifications.php @@ -2,7 +2,7 @@

    - +form->csrf() ?>

    @@ -21,6 +21,6 @@
    - url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + url->link(t('cancel'), 'UserViewController', 'show', array('user_id' => $user['id'])) ?>
    - \ No newline at end of file + diff --git a/sources/app/Template/user/password_reset.php b/sources/app/Template/user_view/password_reset.php similarity index 100% rename from sources/app/Template/user/password_reset.php rename to sources/app/Template/user_view/password_reset.php diff --git a/sources/app/Template/user/profile.php b/sources/app/Template/user_view/profile.php similarity index 100% rename from sources/app/Template/user/profile.php rename to sources/app/Template/user_view/profile.php diff --git a/sources/app/Template/user/sessions.php b/sources/app/Template/user_view/sessions.php similarity index 85% rename from sources/app/Template/user/sessions.php rename to sources/app/Template/user_view/sessions.php index d7fe895..eda3ef7 100644 --- a/sources/app/Template/user/sessions.php +++ b/sources/app/Template/user_view/sessions.php @@ -19,7 +19,7 @@ - +
    order(t('Id'), 'id') ?> order(t('Username'), 'username') ?> order(t('Name'), 'name') ?> order(t('Email'), 'email') ?>
    -   - url->link($this->text->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?> + + + url->link($this->text->e($user['username']), 'UserViewController', 'show', array('user_id' => $user['id'])) ?> text->e($user['name']) ?> @@ -52,7 +55,7 @@ - render('user/dropdown', array('user' => $user)) ?> + render('user_list/dropdown', array('user' => $user)) ?>
    dt->datetime($session['expiration']) ?> text->e($session['ip']) ?> text->e($session['user_agent']) ?>url->link(t('Remove'), 'User', 'removeSession', array('user_id' => $user['id'], 'id' => $session['id']), true) ?>url->link(t('Remove'), 'UserViewController', 'removeSession', array('user_id' => $user['id'], 'id' => $session['id']), true) ?>
    diff --git a/sources/app/Template/user_view/share.php b/sources/app/Template/user_view/share.php new file mode 100644 index 0000000..570b766 --- /dev/null +++ b/sources/app/Template/user_view/share.php @@ -0,0 +1,15 @@ + + + +
    +
      +
    • url->link(t('RSS feed'), 'FeedController', 'user', array('token' => $user['token']), false, '', '', true) ?>
    • +
    • url->link(t('iCal feed'), 'ICalendarController', 'user', array('token' => $user['token']), false, '', '', true) ?>
    • +
    +
    + url->link(t('Disable public access'), 'UserViewController', 'share', array('user_id' => $user['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> + + url->link(t('Enable public access'), 'UserViewController', 'share', array('user_id' => $user['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> + diff --git a/sources/app/Template/user/show.php b/sources/app/Template/user_view/show.php similarity index 75% rename from sources/app/Template/user/show.php rename to sources/app/Template/user_view/show.php index df0affb..fc11f8a 100644 --- a/sources/app/Template/user/show.php +++ b/sources/app/Template/user_view/show.php @@ -15,6 +15,10 @@
  • user->getRoleName($user['role']) ?>
  • +
  • + +
  • dt->datetime($user['lock_expiration_date']) ?>
  • +