summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Api/Me.php2
-rw-r--r--app/Api/Project.php4
-rw-r--r--app/Api/User.php1
-rw-r--r--app/Auth/LdapAuth.php6
-rw-r--r--app/Console/BaseCommand.php (renamed from app/Console/Base.php)4
-rw-r--r--app/Console/CronjobCommand.php (renamed from app/Console/Cronjob.php)2
-rw-r--r--app/Console/LocaleComparatorCommand.php (renamed from app/Console/LocaleComparator.php)2
-rw-r--r--app/Console/LocaleSyncCommand.php (renamed from app/Console/LocaleSync.php)2
-rw-r--r--app/Console/ProjectDailyColumnStatsExportCommand.php (renamed from app/Console/ProjectDailyColumnStatsExport.php)2
-rw-r--r--app/Console/ProjectDailyStatsCalculationCommand.php (renamed from app/Console/ProjectDailyStatsCalculation.php)2
-rw-r--r--app/Console/ResetPasswordCommand.php79
-rw-r--r--app/Console/ResetTwoFactorCommand.php38
-rw-r--r--app/Console/SubtaskExportCommand.php (renamed from app/Console/SubtaskExport.php)2
-rw-r--r--app/Console/TaskExportCommand.php (renamed from app/Console/TaskExport.php)2
-rw-r--r--app/Console/TaskOverdueNotificationCommand.php (renamed from app/Console/TaskOverdueNotification.php)2
-rw-r--r--app/Console/TaskTriggerCommand.php (renamed from app/Console/TaskTrigger.php)2
-rw-r--r--app/Console/TransitionExportCommand.php (renamed from app/Console/TransitionExport.php)2
-rw-r--r--app/Controller/Activity.php5
-rw-r--r--app/Controller/Analytic.php8
-rw-r--r--app/Controller/App.php2
-rw-r--r--app/Controller/AvatarFile.php92
-rw-r--r--app/Controller/Base.php56
-rw-r--r--app/Controller/Board.php21
-rw-r--r--app/Controller/Calendar.php46
-rw-r--r--app/Controller/Doc.php87
-rw-r--r--app/Controller/Feed.php4
-rw-r--r--app/Controller/FileViewer.php26
-rw-r--r--app/Controller/Gantt.php34
-rw-r--r--app/Controller/GroupHelper.php8
-rw-r--r--app/Controller/Ical.php59
-rw-r--r--app/Controller/Listing.php21
-rw-r--r--app/Controller/Oauth.php106
-rw-r--r--app/Controller/ProjectOverview.php21
-rw-r--r--app/Controller/ProjectPermission.php4
-rw-r--r--app/Controller/Search.php32
-rw-r--r--app/Controller/Task.php8
-rw-r--r--app/Controller/TaskHelper.php38
-rw-r--r--app/Controller/UserHelper.php13
-rw-r--r--app/Core/Action/ActionManager.php2
-rw-r--r--app/Core/Base.php33
-rw-r--r--app/Core/Cache/Base.php20
-rw-r--r--app/Core/ExternalLink/ExternalLinkManager.php2
-rw-r--r--app/Core/Filter/CriteriaInterface.php40
-rw-r--r--app/Core/Filter/FilterInterface.php56
-rw-r--r--app/Core/Filter/FormatterInterface.php31
-rw-r--r--app/Core/Filter/Lexer.php153
-rw-r--r--app/Core/Filter/LexerBuilder.php151
-rw-r--r--app/Core/Filter/OrCriteria.php68
-rw-r--r--app/Core/Filter/QueryBuilder.php103
-rw-r--r--app/Core/Helper.php29
-rw-r--r--app/Core/Http/OAuth2.php45
-rw-r--r--app/Core/Http/Response.php32
-rw-r--r--app/Core/Ldap/Client.php44
-rw-r--r--app/Core/Ldap/Query.php6
-rw-r--r--app/Core/Ldap/User.php3
-rw-r--r--app/Core/Lexer.php161
-rw-r--r--app/Core/Session/SessionStorage.php1
-rw-r--r--app/Core/Template.php40
-rw-r--r--app/Core/Thumbnail.php172
-rw-r--r--app/Core/Tool.php74
-rw-r--r--app/Core/User/Avatar/AvatarManager.php7
-rw-r--r--app/Core/User/UserSession.php13
-rw-r--r--app/Filter/BaseDateFilter.php103
-rw-r--r--app/Filter/BaseFilter.php75
-rw-r--r--app/Filter/ProjectActivityCreationDateFilter.php38
-rw-r--r--app/Filter/ProjectActivityCreatorFilter.php65
-rw-r--r--app/Filter/ProjectActivityProjectIdFilter.php38
-rw-r--r--app/Filter/ProjectActivityProjectIdsFilter.php43
-rw-r--r--app/Filter/ProjectActivityProjectNameFilter.php38
-rw-r--r--app/Filter/ProjectActivityTaskIdFilter.php38
-rw-r--r--app/Filter/ProjectActivityTaskStatusFilter.php43
-rw-r--r--app/Filter/ProjectActivityTaskTitleFilter.php25
-rw-r--r--app/Filter/ProjectGroupRoleProjectFilter.php38
-rw-r--r--app/Filter/ProjectGroupRoleUsernameFilter.php44
-rw-r--r--app/Filter/ProjectIdsFilter.php43
-rw-r--r--app/Filter/ProjectStatusFilter.php45
-rw-r--r--app/Filter/ProjectTypeFilter.php45
-rw-r--r--app/Filter/ProjectUserRoleProjectFilter.php38
-rw-r--r--app/Filter/ProjectUserRoleUsernameFilter.php41
-rw-r--r--app/Filter/TaskAssigneeFilter.php75
-rw-r--r--app/Filter/TaskCategoryFilter.php46
-rw-r--r--app/Filter/TaskColorFilter.php60
-rw-r--r--app/Filter/TaskColumnFilter.php44
-rw-r--r--app/Filter/TaskCommentFilter.php41
-rw-r--r--app/Filter/TaskCompletionDateFilter.php38
-rw-r--r--app/Filter/TaskCreationDateFilter.php38
-rw-r--r--app/Filter/TaskCreatorFilter.php74
-rw-r--r--app/Filter/TaskDescriptionFilter.php38
-rw-r--r--app/Filter/TaskDueDateFilter.php41
-rw-r--r--app/Filter/TaskDueDateRangeFilter.php39
-rw-r--r--app/Filter/TaskIdExclusionFilter.php38
-rw-r--r--app/Filter/TaskIdFilter.php38
-rw-r--r--app/Filter/TaskLinkFilter.php85
-rw-r--r--app/Filter/TaskModificationDateFilter.php38
-rw-r--r--app/Filter/TaskProjectFilter.php44
-rw-r--r--app/Filter/TaskProjectsFilter.php43
-rw-r--r--app/Filter/TaskReferenceFilter.php38
-rw-r--r--app/Filter/TaskStartDateFilter.php38
-rw-r--r--app/Filter/TaskStatusFilter.php43
-rw-r--r--app/Filter/TaskSubtaskAssigneeFilter.php140
-rw-r--r--app/Filter/TaskSwimlaneFilter.php50
-rw-r--r--app/Filter/TaskTitleFilter.php46
-rw-r--r--app/Filter/UserNameFilter.php35
-rw-r--r--app/Formatter/BaseFormatter.php37
-rw-r--r--app/Formatter/BaseTaskCalendarFormatter.php (renamed from app/Formatter/TaskFilterCalendarEvent.php)37
-rw-r--r--app/Formatter/BoardFormatter.php56
-rw-r--r--app/Formatter/FormatterInterface.php14
-rw-r--r--app/Formatter/GroupAutoCompleteFormatter.php28
-rw-r--r--app/Formatter/ProjectActivityEventFormatter.php61
-rw-r--r--app/Formatter/ProjectGanttFormatter.php39
-rw-r--r--app/Formatter/SubtaskTimeTrackingCalendarFormatter.php38
-rw-r--r--app/Formatter/TaskAutoCompleteFormatter.php (renamed from app/Formatter/TaskFilterAutoCompleteFormatter.php)12
-rw-r--r--app/Formatter/TaskCalendarFormatter.php (renamed from app/Formatter/TaskFilterCalendarFormatter.php)30
-rw-r--r--app/Formatter/TaskGanttFormatter.php (renamed from app/Formatter/TaskFilterGanttFormatter.php)14
-rw-r--r--app/Formatter/TaskICalFormatter.php (renamed from app/Formatter/TaskFilterICalendarFormatter.php)17
-rw-r--r--app/Formatter/UserAutoCompleteFormatter.php (renamed from app/Formatter/UserFilterAutoCompleteFormatter.php)6
-rw-r--r--app/Helper/AvatarHelper.php22
-rw-r--r--app/Helper/CalendarHelper.php112
-rw-r--r--app/Helper/ICalHelper.php38
-rw-r--r--app/Helper/ProjectActivityHelper.php105
-rw-r--r--app/Helper/ProjectHeaderHelper.php80
-rw-r--r--app/Helper/UserHelper.php2
-rw-r--r--app/Locale/bs_BA/translations.php68
-rw-r--r--app/Locale/cs_CZ/translations.php52
-rw-r--r--app/Locale/da_DK/translations.php52
-rw-r--r--app/Locale/de_DE/translations.php86
-rw-r--r--app/Locale/el_GR/translations.php52
-rw-r--r--app/Locale/es_ES/translations.php52
-rw-r--r--app/Locale/fi_FI/translations.php52
-rw-r--r--app/Locale/fr_FR/translations.php51
-rw-r--r--app/Locale/hu_HU/translations.php52
-rw-r--r--app/Locale/id_ID/translations.php52
-rw-r--r--app/Locale/it_IT/translations.php78
-rw-r--r--app/Locale/ja_JP/translations.php52
-rw-r--r--app/Locale/ko_KR/translations.php1170
-rw-r--r--app/Locale/my_MY/translations.php52
-rw-r--r--app/Locale/nb_NO/translations.php52
-rw-r--r--app/Locale/nl_NL/translations.php52
-rw-r--r--app/Locale/pl_PL/translations.php52
-rw-r--r--app/Locale/pt_BR/translations.php222
-rw-r--r--app/Locale/pt_PT/translations.php52
-rw-r--r--app/Locale/ru_RU/translations.php52
-rw-r--r--app/Locale/sr_Latn_RS/translations.php52
-rw-r--r--app/Locale/sv_SE/translations.php52
-rw-r--r--app/Locale/th_TH/translations.php52
-rw-r--r--app/Locale/tr_TR/translations.php52
-rw-r--r--app/Locale/zh_CN/translations.php52
-rw-r--r--app/Model/AvatarFile.php112
-rw-r--r--app/Model/Base.php24
-rw-r--r--app/Model/Comment.php7
-rw-r--r--app/Model/Config.php2
-rw-r--r--app/Model/File.php20
-rw-r--r--app/Model/Project.php14
-rw-r--r--app/Model/ProjectActivity.php152
-rw-r--r--app/Model/ProjectGroupRoleFilter.php89
-rw-r--r--app/Model/ProjectPermission.php18
-rw-r--r--app/Model/ProjectUserRole.php4
-rw-r--r--app/Model/ProjectUserRoleFilter.php88
-rw-r--r--app/Model/Setting.php1
-rw-r--r--app/Model/SubtaskTimeTracking.php88
-rw-r--r--app/Model/TaskFilter.php745
-rw-r--r--app/Model/TaskFinder.php23
-rw-r--r--app/Model/User.php9
-rw-r--r--app/Model/UserFilter.php80
-rw-r--r--app/Model/UserNotification.php19
-rw-r--r--app/Model/UserNotificationFilter.php9
-rw-r--r--app/Schema/Mysql.php18
-rw-r--r--app/Schema/Postgres.php7
-rw-r--r--app/Schema/Sql/mysql.sql17
-rw-r--r--app/Schema/Sql/postgres.sql67
-rw-r--r--app/Schema/Sqlite.php7
-rw-r--r--app/ServiceProvider/AuthenticationProvider.php1
-rw-r--r--app/ServiceProvider/AvatarProvider.php2
-rw-r--r--app/ServiceProvider/ClassProvider.php13
-rw-r--r--app/ServiceProvider/DatabaseProvider.php4
-rw-r--r--app/ServiceProvider/FilterProvider.php174
-rw-r--r--app/ServiceProvider/HelperProvider.php4
-rw-r--r--app/ServiceProvider/RouteProvider.php3
-rw-r--r--app/Template/activity/filter_dropdown.php14
-rw-r--r--app/Template/activity/project.php40
-rw-r--r--app/Template/analytic/layout.php33
-rw-r--r--app/Template/app/layout.php2
-rw-r--r--app/Template/avatar_file/show.php20
-rw-r--r--app/Template/board/task_avatar.php1
-rw-r--r--app/Template/board/task_footer.php2
-rw-r--r--app/Template/board/view_private.php17
-rw-r--r--app/Template/calendar/show.php6
-rw-r--r--app/Template/comment/show.php2
-rw-r--r--app/Template/event/events.php3
-rw-r--r--app/Template/gantt/project.php7
-rw-r--r--app/Template/listing/show.php14
-rw-r--r--app/Template/project/layout.php27
-rw-r--r--app/Template/project_header/dropdown.php4
-rw-r--r--app/Template/project_header/header.php2
-rw-r--r--app/Template/project_header/views.php2
-rw-r--r--app/Template/project_overview/description.php2
-rw-r--r--app/Template/project_overview/show.php6
-rw-r--r--app/Template/search/activity.php39
-rw-r--r--app/Template/search/index.php4
-rw-r--r--app/Template/task/details.php7
-rw-r--r--app/Template/task/layout.php22
-rw-r--r--app/Template/task/menu.php78
-rw-r--r--app/Template/task/show.php1
-rw-r--r--app/Template/task/sidebar.php3
-rw-r--r--app/Template/task_creation/form.php4
-rw-r--r--app/Template/user/index.php5
-rw-r--r--app/Template/user/notifications.php3
-rw-r--r--app/Template/user/profile.php1
-rw-r--r--app/Template/user/sidebar.php3
-rw-r--r--app/User/Avatar/AvatarFileProvider.php42
-rw-r--r--app/User/Avatar/GravatarProvider.php5
-rw-r--r--app/User/Avatar/LetterAvatarProvider.php7
-rw-r--r--app/common.php1
213 files changed, 6947 insertions, 2855 deletions
diff --git a/app/Api/Me.php b/app/Api/Me.php
index ccc809ed..3d08626a 100644
--- a/app/Api/Me.php
+++ b/app/Api/Me.php
@@ -33,7 +33,7 @@ class Me extends Base
public function getMyActivityStream()
{
$project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
- return $this->projectActivity->getProjects($project_ids, 100);
+ return $this->helper->projectActivity->getProjectsEvents($project_ids, 100);
}
public function createMyPrivateProject($name, $description = null)
diff --git a/app/Api/Project.php b/app/Api/Project.php
index 8e311f7f..846d7046 100644
--- a/app/Api/Project.php
+++ b/app/Api/Project.php
@@ -53,13 +53,13 @@ class Project extends Base
public function getProjectActivities(array $project_ids)
{
- return $this->projectActivity->getProjects($project_ids);
+ return $this->helper->projectActivity->getProjectsEvents($project_ids);
}
public function getProjectActivity($project_id)
{
$this->checkProjectPermission($project_id);
- return $this->projectActivity->getProject($project_id);
+ return $this->helper->projectActivity->getProjectEvents($project_id);
}
public function createProject($name, $description = null)
diff --git a/app/Api/User.php b/app/Api/User.php
index 9b8081d6..6ee935a3 100644
--- a/app/Api/User.php
+++ b/app/Api/User.php
@@ -87,6 +87,7 @@ class User extends \Kanboard\Core\Base
try {
$ldap = LdapClient::connect();
+ $ldap->setLogger($this->logger);
$user = LdapUser::getUser($ldap, $username);
if ($user === null) {
diff --git a/app/Auth/LdapAuth.php b/app/Auth/LdapAuth.php
index b4efbb55..c9423580 100644
--- a/app/Auth/LdapAuth.php
+++ b/app/Auth/LdapAuth.php
@@ -63,10 +63,12 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
try {
$client = LdapClient::connect($this->getLdapUsername(), $this->getLdapPassword());
+ $client->setLogger($this->logger);
+
$user = LdapUser::getUser($client, $this->username);
if ($user === null) {
- $this->logger->info('User not found in LDAP server');
+ $this->logger->info('User ('.$this->username.') not found in LDAP server');
return false;
}
@@ -74,6 +76,8 @@ class LdapAuth extends Base implements PasswordAuthenticationProviderInterface
throw new LogicException('Username not found in LDAP profile, check the parameter LDAP_USER_ATTRIBUTE_USERNAME');
}
+ $this->logger->info('Authenticate user: '.$user->getDn());
+
if ($client->authenticate($user->getDn(), $this->password)) {
$this->userInfo = $user;
return true;
diff --git a/app/Console/Base.php b/app/Console/BaseCommand.php
index 25d48e44..23cdcc9c 100644
--- a/app/Console/Base.php
+++ b/app/Console/BaseCommand.php
@@ -11,6 +11,7 @@ use Symfony\Component\Console\Command\Command;
* @package console
* @author Frederic Guillot
*
+ * @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
* @property \Kanboard\Export\SubtaskExport $subtaskExport
* @property \Kanboard\Export\TaskExport $taskExport
* @property \Kanboard\Export\TransitionExport $transitionExport
@@ -21,11 +22,12 @@ use Symfony\Component\Console\Command\Command;
* @property \Kanboard\Model\ProjectDailyStats $projectDailyStats
* @property \Kanboard\Model\Task $task
* @property \Kanboard\Model\TaskFinder $taskFinder
+ * @property \Kanboard\Model\User $user
* @property \Kanboard\Model\UserNotification $userNotification
* @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
*/
-abstract class Base extends Command
+abstract class BaseCommand extends Command
{
/**
* Container instance
diff --git a/app/Console/Cronjob.php b/app/Console/CronjobCommand.php
index 3a5c5596..dae13af9 100644
--- a/app/Console/Cronjob.php
+++ b/app/Console/CronjobCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\NullOutput;
-class Cronjob extends Base
+class CronjobCommand extends BaseCommand
{
private $commands = array(
'projects:daily-stats',
diff --git a/app/Console/LocaleComparator.php b/app/Console/LocaleComparatorCommand.php
index 8e5e0904..de83714f 100644
--- a/app/Console/LocaleComparator.php
+++ b/app/Console/LocaleComparatorCommand.php
@@ -7,7 +7,7 @@ use RecursiveDirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class LocaleComparator extends Base
+class LocaleComparatorCommand extends BaseCommand
{
const REF_LOCALE = 'fr_FR';
diff --git a/app/Console/LocaleSync.php b/app/Console/LocaleSyncCommand.php
index d62b40b5..11cfbde0 100644
--- a/app/Console/LocaleSync.php
+++ b/app/Console/LocaleSyncCommand.php
@@ -6,7 +6,7 @@ use DirectoryIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class LocaleSync extends Base
+class LocaleSyncCommand extends BaseCommand
{
const REF_LOCALE = 'fr_FR';
diff --git a/app/Console/ProjectDailyColumnStatsExport.php b/app/Console/ProjectDailyColumnStatsExportCommand.php
index 2513fbf1..ced1a374 100644
--- a/app/Console/ProjectDailyColumnStatsExport.php
+++ b/app/Console/ProjectDailyColumnStatsExportCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class ProjectDailyColumnStatsExport extends Base
+class ProjectDailyColumnStatsExportCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/ProjectDailyStatsCalculation.php b/app/Console/ProjectDailyStatsCalculationCommand.php
index 9884cc1c..5b898f02 100644
--- a/app/Console/ProjectDailyStatsCalculation.php
+++ b/app/Console/ProjectDailyStatsCalculationCommand.php
@@ -6,7 +6,7 @@ use Kanboard\Model\Project;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class ProjectDailyStatsCalculation extends Base
+class ProjectDailyStatsCalculationCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/ResetPasswordCommand.php b/app/Console/ResetPasswordCommand.php
new file mode 100644
index 00000000..93dc3761
--- /dev/null
+++ b/app/Console/ResetPasswordCommand.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Kanboard\Console;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\Question;
+
+class ResetPasswordCommand extends BaseCommand
+{
+ protected function configure()
+ {
+ $this
+ ->setName('user:reset-password')
+ ->setDescription('Change user password')
+ ->addArgument('username', InputArgument::REQUIRED, 'Username')
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $helper = $this->getHelper('question');
+ $username = $input->getArgument('username');
+
+ $passwordQuestion = new Question('What is the new password for '.$username.'? (characters are not printed)'.PHP_EOL);
+ $passwordQuestion->setHidden(true);
+ $passwordQuestion->setHiddenFallback(false);
+
+ $password = $helper->ask($input, $output, $passwordQuestion);
+
+ $confirmationQuestion = new Question('Confirmation:'.PHP_EOL);
+ $confirmationQuestion->setHidden(true);
+ $confirmationQuestion->setHiddenFallback(false);
+
+ $confirmation = $helper->ask($input, $output, $confirmationQuestion);
+
+ if ($this->validatePassword($output, $password, $confirmation)) {
+ $this->resetPassword($output, $username, $password);
+ }
+ }
+
+ private function validatePassword(OutputInterface $output, $password, $confirmation)
+ {
+ list($valid, $errors) = $this->passwordResetValidator->validateModification(array(
+ 'password' => $password,
+ 'confirmation' => $confirmation,
+ ));
+
+ if (!$valid) {
+ foreach ($errors as $error_list) {
+ foreach ($error_list as $error) {
+ $output->writeln('<error>'.$error.'</error>');
+ }
+ }
+ }
+
+ return $valid;
+ }
+
+ private function resetPassword(OutputInterface $output, $username, $password)
+ {
+ $userId = $this->user->getIdByUsername($username);
+
+ if (empty($userId)) {
+ $output->writeln('<error>User not found</error>');
+ return false;
+ }
+
+ if (!$this->user->update(array('id' => $userId, 'password' => $password))) {
+ $output->writeln('<error>Unable to update password</error>');
+ return false;
+ }
+
+ $output->writeln('<info>Password updated successfully</info>');
+
+ return true;
+ }
+}
diff --git a/app/Console/ResetTwoFactorCommand.php b/app/Console/ResetTwoFactorCommand.php
new file mode 100644
index 00000000..3bf01e81
--- /dev/null
+++ b/app/Console/ResetTwoFactorCommand.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Console;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ResetTwoFactorCommand extends BaseCommand
+{
+ protected function configure()
+ {
+ $this
+ ->setName('user:reset-2fa')
+ ->setDescription('Remove two-factor authentication for a user')
+ ->addArgument('username', InputArgument::REQUIRED, 'Username');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $username = $input->getArgument('username');
+ $userId = $this->user->getIdByUsername($username);
+
+ if (empty($userId)) {
+ $output->writeln('<error>User not found</error>');
+ return false;
+ }
+
+ if (!$this->user->update(array('id' => $userId, 'twofactor_activated' => 0, 'twofactor_secret' => ''))) {
+ $output->writeln('<error>Unable to update user profile</error>');
+ return false;
+ }
+
+ $output->writeln('<info>Two-factor authentication disabled</info>');
+
+ return true;
+ }
+}
diff --git a/app/Console/SubtaskExport.php b/app/Console/SubtaskExportCommand.php
index aaa95276..986af1a4 100644
--- a/app/Console/SubtaskExport.php
+++ b/app/Console/SubtaskExportCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class SubtaskExport extends Base
+class SubtaskExportCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/TaskExport.php b/app/Console/TaskExportCommand.php
index 4515bf95..789245bc 100644
--- a/app/Console/TaskExport.php
+++ b/app/Console/TaskExportCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class TaskExport extends Base
+class TaskExportCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/TaskOverdueNotification.php b/app/Console/TaskOverdueNotificationCommand.php
index 43be4df8..7d176ab1 100644
--- a/app/Console/TaskOverdueNotification.php
+++ b/app/Console/TaskOverdueNotificationCommand.php
@@ -8,7 +8,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-class TaskOverdueNotification extends Base
+class TaskOverdueNotificationCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/TaskTrigger.php b/app/Console/TaskTriggerCommand.php
index 8d707211..9e9554f9 100644
--- a/app/Console/TaskTrigger.php
+++ b/app/Console/TaskTriggerCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Kanboard\Model\Task;
use Kanboard\Event\TaskListEvent;
-class TaskTrigger extends Base
+class TaskTriggerCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Console/TransitionExport.php b/app/Console/TransitionExportCommand.php
index d9f805a4..265757b3 100644
--- a/app/Console/TransitionExport.php
+++ b/app/Console/TransitionExportCommand.php
@@ -7,7 +7,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class TransitionExport extends Base
+class TransitionExportCommand extends BaseCommand
{
protected function configure()
{
diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php
index db520ebe..47a66e0a 100644
--- a/app/Controller/Activity.php
+++ b/app/Controller/Activity.php
@@ -20,7 +20,7 @@ class Activity extends Base
$project = $this->getProject();
$this->response->html($this->helper->layout->app('activity/project', array(
- 'events' => $this->projectActivity->getProject($project['id']),
+ 'events' => $this->helper->projectActivity->getProjectEvents($project['id']),
'project' => $project,
'title' => t('%s\'s activity', $project['name'])
)));
@@ -38,7 +38,8 @@ class Activity extends Base
$this->response->html($this->helper->layout->task('activity/task', array(
'title' => $task['title'],
'task' => $task,
- 'events' => $this->projectActivity->getTask($task['id']),
+ 'project' => $this->project->getById($task['project_id']),
+ 'events' => $this->helper->projectActivity->getTaskEvents($task['id']),
)));
}
}
diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php
index 6ce062c4..35bc3048 100644
--- a/app/Controller/Analytic.php
+++ b/app/Controller/Analytic.php
@@ -2,6 +2,7 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -44,14 +45,15 @@ class Analytic extends Base
public function compareHours()
{
$project = $this->getProject();
- $params = $this->getProjectFilters('analytic', 'compareHours');
- $query = $this->taskFilter->create()->filterByProject($params['project']['id'])->getQuery();
$paginator = $this->paginator
->setUrl('analytic', 'compareHours', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
- ->setQuery($query)
+ ->setQuery($this->taskQuery
+ ->withFilter(new TaskProjectFilter($project['id']))
+ ->getQuery()
+ )
->calculate();
$this->response->html($this->helper->layout->analytic('analytic/compare_hours', array(
diff --git a/app/Controller/App.php b/app/Controller/App.php
index df1d3c90..01f733ff 100644
--- a/app/Controller/App.php
+++ b/app/Controller/App.php
@@ -157,7 +157,7 @@ class App extends Base
$this->response->html($this->helper->layout->dashboard('app/activity', array(
'title' => t('My activity stream'),
- 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100),
+ 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id']), 100),
'user' => $user,
)));
}
diff --git a/app/Controller/AvatarFile.php b/app/Controller/AvatarFile.php
new file mode 100644
index 00000000..a47cca66
--- /dev/null
+++ b/app/Controller/AvatarFile.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Kanboard\Controller;
+
+use Kanboard\Core\ObjectStorage\ObjectStorageException;
+use Kanboard\Core\Thumbnail;
+
+/**
+ * Avatar File Controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class AvatarFile extends Base
+{
+ /**
+ * Display avatar page
+ */
+ public function show()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->helper->layout->user('avatar_file/show', array(
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Upload Avatar
+ */
+ public function upload()
+ {
+ $user = $this->getUser();
+
+ if (! $this->avatarFile->uploadFile($user['id'], $this->request->getFileInfo('avatar'))) {
+ $this->flash->failure(t('Unable to upload the file.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id'])));
+ }
+
+ /**
+ * Remove Avatar image
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $user = $this->getUser();
+ $this->avatarFile->remove($user['id']);
+ $this->response->redirect($this->helper->url->to('AvatarFile', 'show', array('user_id' => $user['id'])));
+ }
+
+ /**
+ * Show Avatar image (public)
+ */
+ public function image()
+ {
+ $user_id = $this->request->getIntegerParam('user_id');
+ $size = $this->request->getStringParam('size', 48);
+ $filename = $this->avatarFile->getFilename($user_id);
+ $etag = md5($filename.$size);
+
+ $this->response->cache(365 * 86400, $etag);
+ $this->response->contentType('image/jpeg');
+
+ if ($this->request->getHeader('If-None-Match') !== '"'.$etag.'"') {
+ $this->render($filename, $size);
+ } else {
+ $this->response->status(304);
+ }
+ }
+
+ /**
+ * Render thumbnail from object storage
+ *
+ * @access private
+ * @param string $filename
+ * @param integer $size
+ */
+ private function render($filename, $size)
+ {
+ try {
+ $blob = $this->objectStorage->get($filename);
+
+ Thumbnail::createFromString($blob)
+ ->resize($size, $size)
+ ->toOutput();
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+}
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index 2453be18..beb56909 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -287,60 +287,4 @@ abstract class Base extends \Kanboard\Core\Base
return $subtask;
}
-
- /**
- * Common method to get project filters
- *
- * @access protected
- * @param string $controller
- * @param string $action
- * @return array
- */
- protected function getProjectFilters($controller, $action)
- {
- $project = $this->getProject();
- $search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
- $board_selector = $this->projectUserRole->getActiveProjectsByUser($this->userSession->getId());
- unset($board_selector[$project['id']]);
-
- $filters = array(
- 'controller' => $controller,
- 'action' => $action,
- 'project_id' => $project['id'],
- 'search' => urldecode($search),
- );
-
- $this->userSession->setFilters($project['id'], $filters['search']);
-
- return array(
- 'project' => $project,
- 'board_selector' => $board_selector,
- 'filters' => $filters,
- 'title' => $project['name'],
- 'description' => $this->getProjectDescription($project),
- );
- }
-
- /**
- * Get project description
- *
- * @access protected
- * @param array &$project
- * @return string
- */
- protected function getProjectDescription(array &$project)
- {
- if ($project['owner_id'] > 0) {
- $description = t('Project owner: ').'**'.$this->helper->text->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL;
-
- if (! empty($project['description'])) {
- $description .= '***'.PHP_EOL.PHP_EOL;
- $description .= $project['description'];
- }
- } else {
- $description = $project['description'];
- }
-
- return $description;
- }
}
diff --git a/app/Controller/Board.php b/app/Controller/Board.php
index 199f1703..67e99b81 100644
--- a/app/Controller/Board.php
+++ b/app/Controller/Board.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Formatter\BoardFormatter;
+
/**
* Board controller
*
@@ -47,16 +49,19 @@ class Board extends Base
*/
public function show()
{
- $params = $this->getProjectFilters('board', 'show');
+ $project = $this->getProject();
+ $search = $this->helper->projectHeader->getSearchQuery($project);
$this->response->html($this->helper->layout->app('board/view_private', array(
- 'categories_list' => $this->category->getList($params['project']['id'], false),
- 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
- 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
- 'swimlanes' => $this->taskFilter->search($params['filters']['search'])->getBoard($params['project']['id']),
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
- ) + $params));
+ 'swimlanes' => $this->taskLexer
+ ->build($search)
+ ->format(BoardFormatter::getInstance($this->container)->setProjectId($project['id']))
+ )));
}
/**
@@ -177,9 +182,11 @@ class Board extends Base
{
return $this->template->render('board/table_container', array(
'project' => $this->project->getById($project_id),
- 'swimlanes' => $this->taskFilter->search($this->userSession->getFilters($project_id))->getBoard($project_id),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
+ 'swimlanes' => $this->taskLexer
+ ->build($this->userSession->getFilters($project_id))
+ ->format(BoardFormatter::getInstance($this->container)->setProjectId($project_id))
));
}
}
diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php
index a0a25e41..2517286d 100644
--- a/app/Controller/Calendar.php
+++ b/app/Controller/Calendar.php
@@ -2,6 +2,9 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskStatusFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -20,9 +23,14 @@ class Calendar extends Base
*/
public function show()
{
+ $project = $this->getProject();
+
$this->response->html($this->helper->layout->app('calendar/show', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
'check_interval' => $this->config->get('board_private_refresh_interval'),
- ) + $this->getProjectFilters('calendar', 'show')));
+ )));
}
/**
@@ -35,21 +43,11 @@ class Calendar extends Base
$project_id = $this->request->getIntegerParam('project_id');
$start = $this->request->getStringParam('start');
$end = $this->request->getStringParam('end');
+ $search = $this->userSession->getFilters($project_id);
+ $queryBuilder = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project_id));
- // Common filter
- $filter = $this->taskFilterCalendarFormatter
- ->search($this->userSession->getFilters($project_id))
- ->filterByProject($project_id);
-
- // Tasks
- if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
- $events = $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format();
- } else {
- $events = $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format();
- }
-
- // Tasks with due date
- $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format());
+ $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
+ $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
$events = $this->hook->merge('controller:calendar:project:events', $events, array(
'project_id' => $project_id,
@@ -70,21 +68,15 @@ class Calendar extends Base
$user_id = $this->request->getIntegerParam('user_id');
$start = $this->request->getStringParam('start');
$end = $this->request->getStringParam('end');
- $filter = $this->taskFilterCalendarFormatter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN);
-
- // Task with due date
- $events = $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format();
+ $queryBuilder = $this->taskQuery
+ ->withFilter(new TaskAssigneeFilter($user_id))
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN));
- // Tasks
- if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') {
- $events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format());
- } else {
- $events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format());
- }
+ $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
+ $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
- // Subtasks time tracking
if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) {
- $events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end));
+ $events = array_merge($events, $this->helper->calendar->getSubtaskTimeTrackingEvents($user_id, $start, $end));
}
$events = $this->hook->merge('controller:calendar:user:events', $events, array(
diff --git a/app/Controller/Doc.php b/app/Controller/Doc.php
index f85326ac..00b9e585 100644
--- a/app/Controller/Doc.php
+++ b/app/Controller/Doc.php
@@ -5,54 +5,32 @@ namespace Kanboard\Controller;
use Parsedown;
/**
- * Documentation controller
+ * Documentation Viewer
*
* @package controller
* @author Frederic Guillot
*/
class Doc extends Base
{
- private function readFile($filename)
- {
- $url = $this->helper->url;
- $data = file_get_contents($filename);
- list($title, ) = explode("\n", $data, 2);
-
- $replaceUrl = function (array $matches) use ($url) {
- return '('.$url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')';
- };
-
- $content = preg_replace_callback('/\((.*.markdown)\)/', $replaceUrl, $data);
-
- return array(
- 'content' => Parsedown::instance()->text($content),
- 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title,
- );
- }
-
public function show()
{
$page = $this->request->getStringParam('file', 'index');
- if (! preg_match('/^[a-z0-9\-]+/', $page)) {
+ if (!preg_match('/^[a-z0-9\-]+/', $page)) {
$page = 'index';
}
- $filenames = array(__DIR__.'/../../doc/'.$page.'.markdown');
- $filename = __DIR__.'/../../doc/index.markdown';
-
if ($this->config->getCurrentLanguage() === 'fr_FR') {
- array_unshift($filenames, __DIR__.'/../../doc/fr/'.$page.'.markdown');
+ $filename = __DIR__.'/../../doc/fr/' . $page . '.markdown';
+ } else {
+ $filename = __DIR__ . '/../../doc/' . $page . '.markdown';
}
- foreach ($filenames as $file) {
- if (file_exists($file)) {
- $filename = $file;
- break;
- }
+ if (!file_exists($filename)) {
+ $filename = __DIR__.'/../../doc/index.markdown';
}
- $this->response->html($this->helper->layout->app('doc/show', $this->readFile($filename)));
+ $this->response->html($this->helper->layout->app('doc/show', $this->render($filename)));
}
/**
@@ -62,4 +40,53 @@ class Doc extends Base
{
$this->response->html($this->template->render('config/keyboard_shortcuts'));
}
+
+ /**
+ * Prepare Markdown file
+ *
+ * @access private
+ * @param string $filename
+ * @return array
+ */
+ private function render($filename)
+ {
+ $data = file_get_contents($filename);
+ $content = preg_replace_callback('/\((.*.markdown)\)/', array($this, 'replaceMarkdownUrl'), $data);
+ $content = preg_replace_callback('/\((screenshots.*\.png)\)/', array($this, 'replaceImageUrl'), $content);
+
+ list($title, ) = explode("\n", $data, 2);
+
+ return array(
+ 'content' => Parsedown::instance()->text($content),
+ 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title,
+ );
+ }
+
+ /**
+ * Regex callback to replace Markdown links
+ *
+ * @access public
+ * @param array $matches
+ * @return string
+ */
+ public function replaceMarkdownUrl(array $matches)
+ {
+ return '('.$this->helper->url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')';
+ }
+
+ /**
+ * Regex callback to replace image links
+ *
+ * @access public
+ * @param array $matches
+ * @return string
+ */
+ public function replaceImageUrl(array $matches)
+ {
+ if ($this->config->getCurrentLanguage() === 'fr_FR') {
+ return '('.$this->helper->url->base().'doc/fr/'.$matches[1].')';
+ }
+
+ return '('.$this->helper->url->base().'doc/'.$matches[1].')';
+ }
}
diff --git a/app/Controller/Feed.php b/app/Controller/Feed.php
index 8457c383..f8b3d320 100644
--- a/app/Controller/Feed.php
+++ b/app/Controller/Feed.php
@@ -26,7 +26,7 @@ class Feed extends Base
}
$this->response->xml($this->template->render('feed/user', array(
- 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])),
+ 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id'])),
'user' => $user,
)));
}
@@ -47,7 +47,7 @@ class Feed extends Base
}
$this->response->xml($this->template->render('feed/project', array(
- 'events' => $this->projectActivity->getProject($project['id']),
+ 'events' => $this->helper->projectActivity->getProjectEvents($project['id']),
'project' => $project,
)));
}
diff --git a/app/Controller/FileViewer.php b/app/Controller/FileViewer.php
index bc91c3d8..3be4ea14 100644
--- a/app/Controller/FileViewer.php
+++ b/app/Controller/FileViewer.php
@@ -66,9 +66,16 @@ class FileViewer extends Base
*/
public function image()
{
+ $file = $this->getFile();
+ $etag = md5($file['path']);
+ $this->response->contentType($this->helper->file->getImageMimeType($file['name']));
+ $this->response->cache(5 * 86400, $etag);
+
+ if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
+ return $this->response->status(304);
+ }
+
try {
- $file = $this->getFile();
- $this->response->contentType($this->helper->file->getImageMimeType($file['name']));
$this->objectStorage->output($file['path']);
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
@@ -82,12 +89,21 @@ class FileViewer extends Base
*/
public function thumbnail()
{
+ $file = $this->getFile();
+ $model = $file['model'];
+ $filename = $this->$model->getThumbnailPath($file['path']);
+ $etag = md5($filename);
+
+ $this->response->cache(5 * 86400, $etag);
$this->response->contentType('image/jpeg');
+ if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
+ return $this->response->status(304);
+ }
+
try {
- $file = $this->getFile();
- $model = $file['model'];
- $this->objectStorage->output($this->$model->getThumbnailPath($file['path']));
+
+ $this->objectStorage->output($filename);
} catch (ObjectStorageException $e) {
$this->logger->error($e->getMessage());
diff --git a/app/Controller/Gantt.php b/app/Controller/Gantt.php
index 9ffa277f..5e9ad55e 100644
--- a/app/Controller/Gantt.php
+++ b/app/Controller/Gantt.php
@@ -2,7 +2,14 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\ProjectIdsFilter;
+use Kanboard\Filter\ProjectStatusFilter;
+use Kanboard\Filter\ProjectTypeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Formatter\ProjectGanttFormatter;
+use Kanboard\Formatter\TaskGanttFormatter;
use Kanboard\Model\Task as TaskModel;
+use Kanboard\Model\Project as ProjectModel;
/**
* Gantt controller
@@ -17,14 +24,16 @@ class Gantt extends Base
*/
public function projects()
{
- if ($this->userSession->isAdmin()) {
- $project_ids = $this->project->getAllIds();
- } else {
- $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
- }
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $filter = $this->projectQuery
+ ->withFilter(new ProjectTypeFilter(ProjectModel::TYPE_TEAM))
+ ->withFilter(new ProjectStatusFilter(ProjectModel::ACTIVE))
+ ->withFilter(new ProjectIdsFilter($project_ids));
+
+ $filter->getQuery()->asc(ProjectModel::TABLE.'.start_date');
$this->response->html($this->helper->layout->app('gantt/projects', array(
- 'projects' => $this->projectGanttFormatter->filter($project_ids)->format(),
+ 'projects' => $filter->format(new ProjectGanttFormatter($this->container)),
'title' => t('Gantt chart for all projects'),
)));
}
@@ -54,9 +63,10 @@ class Gantt extends Base
*/
public function project()
{
- $params = $this->getProjectFilters('gantt', 'project');
- $filter = $this->taskFilterGanttFormatter->search($params['filters']['search'])->filterByProject($params['project']['id']);
+ $project = $this->getProject();
+ $search = $this->helper->projectHeader->getSearchQuery($project);
$sorting = $this->request->getStringParam('sorting', 'board');
+ $filter = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project['id']));
if ($sorting === 'date') {
$filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation');
@@ -64,10 +74,12 @@ class Gantt extends Base
$filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position');
}
- $this->response->html($this->helper->layout->app('gantt/project', $params + array(
- 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
+ $this->response->html($this->helper->layout->app('gantt/project', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
'sorting' => $sorting,
- 'tasks' => $filter->format(),
+ 'tasks' => $filter->format(new TaskGanttFormatter($this->container)),
)));
}
diff --git a/app/Controller/GroupHelper.php b/app/Controller/GroupHelper.php
index 34f522a6..429614c2 100644
--- a/app/Controller/GroupHelper.php
+++ b/app/Controller/GroupHelper.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Formatter\GroupAutoCompleteFormatter;
+
/**
* Group Helper
*
@@ -11,14 +13,14 @@ namespace Kanboard\Controller;
class GroupHelper extends Base
{
/**
- * Group autocompletion (Ajax)
+ * Group auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $groups = $this->groupManager->find($search);
- $this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format());
+ $formatter = new GroupAutoCompleteFormatter($this->groupManager->find($search));
+ $this->response->json($formatter->format());
}
}
diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php
index f1ea6d8f..8fe97b46 100644
--- a/app/Controller/Ical.php
+++ b/app/Controller/Ical.php
@@ -2,7 +2,11 @@
namespace Kanboard\Controller;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskStatusFilter;
+use Kanboard\Formatter\TaskICalFormatter;
use Kanboard\Model\Task as TaskModel;
use Eluceo\iCal\Component\Calendar as iCalendar;
@@ -30,10 +34,11 @@ class Ical extends Base
}
// Common filter
- $filter = $this->taskFilterICalendarFormatter
- ->create()
- ->filterByStatus(TaskModel::STATUS_OPEN)
- ->filterByOwner($user['id']);
+ $queryBuilder = new QueryBuilder();
+ $queryBuilder
+ ->withQuery($this->taskFinder->getICalQuery())
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
+ ->withFilter(new TaskAssigneeFilter($user['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard');
@@ -41,7 +46,7 @@ class Ical extends Base
$calendar->setDescription($user['name'] ?: $user['username']);
$calendar->setPublishedTTL('PT1H');
- $this->renderCalendar($filter, $calendar);
+ $this->renderCalendar($queryBuilder, $calendar);
}
/**
@@ -60,10 +65,11 @@ class Ical extends Base
}
// Common filter
- $filter = $this->taskFilterICalendarFormatter
- ->create()
- ->filterByStatus(TaskModel::STATUS_OPEN)
- ->filterByProject($project['id']);
+ $queryBuilder = new QueryBuilder();
+ $queryBuilder
+ ->withQuery($this->taskFinder->getICalQuery())
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
+ ->withFilter(new TaskProjectFilter($project['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard');
@@ -71,7 +77,7 @@ class Ical extends Base
$calendar->setDescription($project['name']);
$calendar->setPublishedTTL('PT1H');
- $this->renderCalendar($filter, $calendar);
+ $this->renderCalendar($queryBuilder, $calendar);
}
/**
@@ -79,37 +85,14 @@ class Ical extends Base
*
* @access private
*/
- private function renderCalendar(TaskFilter $filter, iCalendar $calendar)
+ private function renderCalendar(QueryBuilder $queryBuilder, iCalendar $calendar)
{
$start = $this->request->getStringParam('start', strtotime('-2 month'));
$end = $this->request->getStringParam('end', strtotime('+6 months'));
- // Tasks
- if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
- $filter
- ->copy()
- ->filterByCreationDateRange($start, $end)
- ->setColumns('date_creation', 'date_completed')
- ->setCalendar($calendar)
- ->addDateTimeEvents();
- } else {
- $filter
- ->copy()
- ->filterByStartDateRange($start, $end)
- ->setColumns('date_started', 'date_completed')
- ->setCalendar($calendar)
- ->addDateTimeEvents($calendar);
- }
-
- // Tasks with due date
- $filter
- ->copy()
- ->filterByDueDateRange($start, $end)
- ->setColumns('date_due')
- ->setCalendar($calendar)
- ->addFullDayEvents($calendar);
+ $this->helper->ical->addTaskDateDueEvents($queryBuilder, $calendar, $start, $end);
- $this->response->contentType('text/calendar; charset=utf-8');
- echo $filter->setCalendar($calendar)->format();
+ $formatter = new TaskICalFormatter($this->container);
+ $this->response->ical($formatter->setCalendar($calendar)->format());
}
}
diff --git a/app/Controller/Listing.php b/app/Controller/Listing.php
index c784dd50..2024ff03 100644
--- a/app/Controller/Listing.php
+++ b/app/Controller/Listing.php
@@ -2,6 +2,7 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -19,22 +20,26 @@ class Listing extends Base
*/
public function show()
{
- $params = $this->getProjectFilters('listing', 'show');
- $query = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id'])->getQuery();
+ $project = $this->getProject();
+ $search = $this->helper->projectHeader->getSearchQuery($project);
$paginator = $this->paginator
- ->setUrl('listing', 'show', array('project_id' => $params['project']['id']))
+ ->setUrl('listing', 'show', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
->setDirection('DESC')
- ->setQuery($query)
+ ->setQuery($this->taskLexer
+ ->build($search)
+ ->withFilter(new TaskProjectFilter($project['id']))
+ ->getQuery()
+ )
->calculate();
- $this->response->html($this->helper->layout->app('listing/show', $params + array(
+ $this->response->html($this->helper->layout->app('listing/show', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
'paginator' => $paginator,
- 'categories_list' => $this->category->getList($params['project']['id'], false),
- 'users_list' => $this->projectUserRole->getAssignableUsersList($params['project']['id'], false),
- 'custom_filters_list' => $this->customFilter->getAll($params['project']['id'], $this->userSession->getId()),
)));
}
}
diff --git a/app/Controller/Oauth.php b/app/Controller/Oauth.php
index 452faecd..12b91144 100644
--- a/app/Controller/Oauth.php
+++ b/app/Controller/Oauth.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Core\Security\OAuthAuthenticationProviderInterface;
+
/**
* OAuth controller
*
@@ -11,25 +13,6 @@ namespace Kanboard\Controller;
class Oauth extends Base
{
/**
- * Unlink external account
- *
- * @access public
- */
- public function unlink()
- {
- $backend = $this->request->getStringParam('backend');
- $this->checkCSRFParam();
-
- if ($this->authenticationManager->getProvider($backend)->unlink($this->userSession->getId())) {
- $this->flash->success(t('Your external account is not linked anymore to your profile.'));
- } else {
- $this->flash->failure(t('Unable to unlink your external account.'));
- }
-
- $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
- }
-
- /**
* Redirect to the provider if no code received
*
* @access private
@@ -38,9 +21,10 @@ class Oauth extends Base
protected function step1($provider)
{
$code = $this->request->getStringParam('code');
+ $state = $this->request->getStringParam('state');
if (! empty($code)) {
- $this->step2($provider, $code);
+ $this->step2($provider, $code, $state);
} else {
$this->response->redirect($this->authenticationManager->getProvider($provider)->getService()->getAuthorizationUrl());
}
@@ -50,34 +34,44 @@ class Oauth extends Base
* Link or authenticate the user
*
* @access protected
- * @param string $provider
+ * @param string $providerName
* @param string $code
+ * @param string $state
*/
- protected function step2($provider, $code)
+ protected function step2($providerName, $code, $state)
{
- $this->authenticationManager->getProvider($provider)->setCode($code);
+ $provider = $this->authenticationManager->getProvider($providerName);
+ $provider->setCode($code);
+ $hasValidState = $provider->getService()->isValidateState($state);
if ($this->userSession->isLogged()) {
- $this->link($provider);
+ if ($hasValidState) {
+ $this->link($provider);
+ } else {
+ $this->flash->failure(t('The OAuth2 state parameter is invalid'));
+ $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
+ }
+ } else {
+ if ($hasValidState) {
+ $this->authenticate($providerName);
+ } else {
+ $this->authenticationFailure(t('The OAuth2 state parameter is invalid'));
+ }
}
-
- $this->authenticate($provider);
}
/**
* Link the account
*
* @access protected
- * @param string $provider
+ * @param OAuthAuthenticationProviderInterface $provider
*/
- protected function link($provider)
+ protected function link(OAuthAuthenticationProviderInterface $provider)
{
- $authProvider = $this->authenticationManager->getProvider($provider);
-
- if (! $authProvider->authenticate()) {
+ if (! $provider->authenticate()) {
$this->flash->failure(t('External authentication failed'));
} else {
- $this->userProfile->assign($this->userSession->getId(), $authProvider->getUser());
+ $this->userProfile->assign($this->userSession->getId(), $provider->getUser());
$this->flash->success(t('Your external account is linked to your profile successfully.'));
}
@@ -85,22 +79,52 @@ class Oauth extends Base
}
/**
+ * Unlink external account
+ *
+ * @access public
+ */
+ public function unlink()
+ {
+ $backend = $this->request->getStringParam('backend');
+ $this->checkCSRFParam();
+
+ if ($this->authenticationManager->getProvider($backend)->unlink($this->userSession->getId())) {
+ $this->flash->success(t('Your external account is not linked anymore to your profile.'));
+ } else {
+ $this->flash->failure(t('Unable to unlink your external account.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId())));
+ }
+
+ /**
* Authenticate the account
*
* @access protected
- * @param string $provider
+ * @param string $providerName
*/
- protected function authenticate($provider)
+ protected function authenticate($providerName)
{
- if ($this->authenticationManager->oauthAuthentication($provider)) {
+ if ($this->authenticationManager->oauthAuthentication($providerName)) {
$this->response->redirect($this->helper->url->to('app', 'index'));
} else {
- $this->response->html($this->helper->layout->app('auth/index', array(
- 'errors' => array('login' => t('External authentication failed')),
- 'values' => array(),
- 'no_layout' => true,
- 'title' => t('Login')
- )));
+ $this->authenticationFailure(t('External authentication failed'));
}
}
+
+ /**
+ * Show login failure page
+ *
+ * @access protected
+ * @param string $message
+ */
+ protected function authenticationFailure($message)
+ {
+ $this->response->html($this->helper->layout->app('auth/index', array(
+ 'errors' => array('login' => $message),
+ 'values' => array(),
+ 'no_layout' => true,
+ 'title' => t('Login')
+ )));
+ }
}
diff --git a/app/Controller/ProjectOverview.php b/app/Controller/ProjectOverview.php
index b0687ed3..b2bb33d6 100644
--- a/app/Controller/ProjectOverview.php
+++ b/app/Controller/ProjectOverview.php
@@ -15,15 +15,18 @@ class ProjectOverview extends Base
*/
public function show()
{
- $params = $this->getProjectFilters('ProjectOverview', 'show');
- $params['users'] = $this->projectUserRole->getAllUsersGroupedByRole($params['project']['id']);
- $params['roles'] = $this->role->getProjectRoles();
- $params['events'] = $this->projectActivity->getProject($params['project']['id'], 10);
- $params['images'] = $this->projectFile->getAllImages($params['project']['id']);
- $params['files'] = $this->projectFile->getAllDocuments($params['project']['id']);
+ $project = $this->getProject();
+ $this->project->getColumnStats($project);
- $this->project->getColumnStats($params['project']);
-
- $this->response->html($this->helper->layout->app('project_overview/show', $params));
+ $this->response->html($this->helper->layout->app('project_overview/show', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
+ 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']),
+ 'roles' => $this->role->getProjectRoles(),
+ 'events' => $this->helper->projectActivity->getProjectEvents($project['id'], 10),
+ 'images' => $this->projectFile->getAllImages($project['id']),
+ 'files' => $this->projectFile->getAllDocuments($project['id']),
+ )));
}
}
diff --git a/app/Controller/ProjectPermission.php b/app/Controller/ProjectPermission.php
index 800da02f..e203c0db 100644
--- a/app/Controller/ProjectPermission.php
+++ b/app/Controller/ProjectPermission.php
@@ -83,7 +83,9 @@ class ProjectPermission extends Base
$project = $this->getProject();
$values = $this->request->getValues();
- if ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) {
+ if (empty($values['user_id'])) {
+ $this->flash->failure(t('User not found.'));
+ } elseif ($this->projectUserRole->addUser($values['project_id'], $values['user_id'], $values['role'])) {
$this->flash->success(t('Project updated successfully.'));
} else {
$this->flash->failure(t('Unable to update this project.'));
diff --git a/app/Controller/Search.php b/app/Controller/Search.php
index 9b9b9e65..a42e9d3d 100644
--- a/app/Controller/Search.php
+++ b/app/Controller/Search.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectsFilter;
+
/**
* Search controller
*
@@ -23,14 +25,12 @@ class Search extends Base
->setDirection('DESC');
if ($search !== '' && ! empty($projects)) {
- $query = $this
- ->taskFilter
- ->search($search)
- ->filterByProjects(array_keys($projects))
- ->getQuery();
-
$paginator
- ->setQuery($query)
+ ->setQuery($this->taskLexer
+ ->build($search)
+ ->withFilter(new TaskProjectsFilter(array_keys($projects)))
+ ->getQuery()
+ )
->calculate();
$nb_tasks = $paginator->getTotal();
@@ -46,4 +46,22 @@ class Search extends Base
'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
+
+ public function activity()
+ {
+ $search = urldecode($this->request->getStringParam('search'));
+ $events = $this->helper->projectActivity->searchEvents($search);
+ $nb_events = count($events);
+
+ $this->response->html($this->helper->layout->app('search/activity', array(
+ 'values' => array(
+ 'search' => $search,
+ 'controller' => 'search',
+ 'action' => 'activity',
+ ),
+ 'title' => t('Search in activity stream').($nb_events > 0 ? ' ('.$nb_events.')' : ''),
+ 'nb_events' => $nb_events,
+ 'events' => $events,
+ )));
+ }
}
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index dc10604e..902a32d6 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -71,17 +71,16 @@ class Task extends Base
$values = $this->dateParser->format($values, array('date_started'), $this->config->get('application_datetime_format', DateParser::DATE_TIME_FORMAT));
$this->response->html($this->helper->layout->task('task/show', array(
+ 'task' => $task,
'project' => $this->project->getById($task['project_id']),
+ 'values' => $values,
'files' => $this->taskFile->getAllDocuments($task['id']),
'images' => $this->taskFile->getAllImages($task['id']),
'comments' => $this->comment->getAll($task['id'], $this->userSession->getCommentSorting()),
'subtasks' => $subtasks,
'internal_links' => $this->taskLink->getAllGroupedByLabel($task['id']),
'external_links' => $this->taskExternalLink->getAll($task['id']),
- 'task' => $task,
- 'values' => $values,
'link_label_list' => $this->link->getList(0, false),
- 'users_list' => $this->projectUserRole->getAssignableUsersList($task['project_id'], true, false, false),
)));
}
@@ -96,6 +95,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/analytics', array(
'task' => $task,
+ 'project' => $this->project->getById($task['project_id']),
'lead_time' => $this->taskAnalytic->getLeadTime($task),
'cycle_time' => $this->taskAnalytic->getCycleTime($task),
'time_spent_columns' => $this->taskAnalytic->getTimeSpentByColumn($task),
@@ -121,6 +121,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/time_tracking_details', array(
'task' => $task,
+ 'project' => $this->project->getById($task['project_id']),
'subtask_paginator' => $subtask_paginator,
)));
}
@@ -136,6 +137,7 @@ class Task extends Base
$this->response->html($this->helper->layout->task('task/transitions', array(
'task' => $task,
+ 'project' => $this->project->getById($task['project_id']),
'transitions' => $this->transition->getAllByTask($task['id']),
)));
}
diff --git a/app/Controller/TaskHelper.php b/app/Controller/TaskHelper.php
index 7e340a6a..6835ab2b 100644
--- a/app/Controller/TaskHelper.php
+++ b/app/Controller/TaskHelper.php
@@ -2,6 +2,12 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskIdExclusionFilter;
+use Kanboard\Filter\TaskIdFilter;
+use Kanboard\Filter\TaskProjectsFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Formatter\TaskAutoCompleteFormatter;
+
/**
* Task Ajax Helper
*
@@ -11,31 +17,33 @@ namespace Kanboard\Controller;
class TaskHelper extends Base
{
/**
- * Task autocompletion (Ajax)
+ * Task auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $exclude_task_id = $this->request->getIntegerParam('exclude_task_id');
- if (empty($projects)) {
+ if (empty($project_ids)) {
$this->response->json(array());
- }
+ } else {
- $filter = $this->taskFilterAutoCompleteFormatter
- ->create()
- ->filterByProjects($projects)
- ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
+ $filter = $this->taskQuery->withFilter(new TaskProjectsFilter($project_ids));
- // Search by task id or by title
- if (ctype_digit($search)) {
- $filter->filterById($search);
- } else {
- $filter->filterByTitle($search);
- }
+ if (! empty($exclude_task_id)) {
+ $filter->withFilter(new TaskIdExclusionFilter(array($exclude_task_id)));
+ }
+
+ if (ctype_digit($search)) {
+ $filter->withFilter(new TaskIdFilter($search));
+ } else {
+ $filter->withFilter(new TaskTitleFilter($search));
+ }
- $this->response->json($filter->format());
+ $this->response->json($filter->format(new TaskAutoCompleteFormatter($this->container)));
+ }
}
}
diff --git a/app/Controller/UserHelper.php b/app/Controller/UserHelper.php
index 041ed2c8..47bbe554 100644
--- a/app/Controller/UserHelper.php
+++ b/app/Controller/UserHelper.php
@@ -2,6 +2,10 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\UserNameFilter;
+use Kanboard\Formatter\UserAutoCompleteFormatter;
+use Kanboard\Model\User as UserModel;
+
/**
* User Helper
*
@@ -11,19 +15,20 @@ namespace Kanboard\Controller;
class UserHelper extends Base
{
/**
- * User autocompletion (Ajax)
+ * User auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format();
- $this->response->json($users);
+ $filter = $this->userQuery->withFilter(new UserNameFilter($search));
+ $filter->getQuery()->asc(UserModel::TABLE.'.name')->asc(UserModel::TABLE.'.username');
+ $this->response->json($filter->format(new UserAutoCompleteFormatter($this->container)));
}
/**
- * User mention autocompletion (Ajax)
+ * User mention auto-completion (Ajax)
*
* @access public
*/
diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php
index f1ea8abe..dfa5a140 100644
--- a/app/Core/Action/ActionManager.php
+++ b/app/Core/Action/ActionManager.php
@@ -18,7 +18,7 @@ class ActionManager extends Base
* List of automatic actions
*
* @access private
- * @var array
+ * @var ActionBase[]
*/
private $actions = array();
diff --git a/app/Core/Base.php b/app/Core/Base.php
index f87f271a..2b619af5 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -48,18 +48,11 @@ use Pimple\Container;
* @property \Kanboard\Core\User\UserSession $userSession
* @property \Kanboard\Core\DateParser $dateParser
* @property \Kanboard\Core\Helper $helper
- * @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Template $template
- * @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter
- * @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter
- * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter
- * @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter
- * @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter
- * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter
- * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
* @property \Kanboard\Model\Action $action
* @property \Kanboard\Model\ActionParameter $actionParameter
+ * @property \Kanboard\Model\AvatarFile $avatarFile
* @property \Kanboard\Model\Board $board
* @property \Kanboard\Model\Category $category
* @property \Kanboard\Model\Color $color
@@ -84,7 +77,6 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectMetadata $projectMetadata
* @property \Kanboard\Model\ProjectPermission $projectPermission
* @property \Kanboard\Model\ProjectUserRole $projectUserRole
- * @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter
* @property \Kanboard\Model\ProjectGroupRole $projectGroupRole
* @property \Kanboard\Model\ProjectNotification $projectNotification
* @property \Kanboard\Model\ProjectNotificationType $projectNotificationType
@@ -98,7 +90,6 @@ use Pimple\Container;
* @property \Kanboard\Model\TaskDuplication $taskDuplication
* @property \Kanboard\Model\TaskExternalLink $taskExternalLink
* @property \Kanboard\Model\TaskFinder $taskFinder
- * @property \Kanboard\Model\TaskFilter $taskFilter
* @property \Kanboard\Model\TaskLink $taskLink
* @property \Kanboard\Model\TaskModification $taskModification
* @property \Kanboard\Model\TaskPermission $taskPermission
@@ -136,6 +127,14 @@ use Pimple\Container;
* @property \Kanboard\Export\SubtaskExport $subtaskExport
* @property \Kanboard\Export\TaskExport $taskExport
* @property \Kanboard\Export\TransitionExport $transitionExport
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectActivityQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $userQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery
+ * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer
+ * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer
* @property \Psr\Log\LoggerInterface $logger
* @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
@@ -172,4 +171,18 @@ abstract class Base
{
return $this->container[$name];
}
+
+ /**
+ * Get object instance
+ *
+ * @static
+ * @access public
+ * @param Container $container
+ * @return static
+ */
+ public static function getInstance(Container $container)
+ {
+ $self = new static($container);
+ return $self;
+ }
}
diff --git a/app/Core/Cache/Base.php b/app/Core/Cache/Base.php
index 2879f1f1..d62b8507 100644
--- a/app/Core/Cache/Base.php
+++ b/app/Core/Cache/Base.php
@@ -11,26 +11,6 @@ namespace Kanboard\Core\Cache;
abstract class Base
{
/**
- * Fetch value from cache
- *
- * @abstract
- * @access public
- * @param string $key
- * @return mixed Null when not found, cached value otherwise
- */
- abstract public function get($key);
-
- /**
- * Save a new value in the cache
- *
- * @abstract
- * @access public
- * @param string $key
- * @param mixed $value
- */
- abstract public function set($key, $value);
-
- /**
* Proxy cache
*
* Note: Arguments must be scalar types
diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php
index 1fa423c2..804e6b34 100644
--- a/app/Core/ExternalLink/ExternalLinkManager.php
+++ b/app/Core/ExternalLink/ExternalLinkManager.php
@@ -23,7 +23,7 @@ class ExternalLinkManager extends Base
* Registered providers
*
* @access private
- * @var array
+ * @var ExternalLinkProviderInterface[]
*/
private $providers = array();
diff --git a/app/Core/Filter/CriteriaInterface.php b/app/Core/Filter/CriteriaInterface.php
new file mode 100644
index 00000000..009c4bd3
--- /dev/null
+++ b/app/Core/Filter/CriteriaInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Criteria Interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface CriteriaInterface
+{
+ /**
+ * Set the Query
+ *
+ * @access public
+ * @param Table $query
+ * @return CriteriaInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Set filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return CriteriaInterface
+ */
+ public function withFilter(FilterInterface $filter);
+
+ /**
+ * Apply condition
+ *
+ * @access public
+ * @return CriteriaInterface
+ */
+ public function apply();
+}
diff --git a/app/Core/Filter/FilterInterface.php b/app/Core/Filter/FilterInterface.php
new file mode 100644
index 00000000..7b66ec28
--- /dev/null
+++ b/app/Core/Filter/FilterInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Filter Interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface FilterInterface
+{
+ /**
+ * BaseFilter constructor
+ *
+ * @access public
+ * @param mixed $value
+ */
+ public function __construct($value = null);
+
+ /**
+ * Set the value
+ *
+ * @access public
+ * @param string $value
+ * @return FilterInterface
+ */
+ public function withValue($value);
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FilterInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes();
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply();
+}
diff --git a/app/Core/Filter/FormatterInterface.php b/app/Core/Filter/FormatterInterface.php
new file mode 100644
index 00000000..b7c04c51
--- /dev/null
+++ b/app/Core/Filter/FormatterInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Formatter interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface FormatterInterface
+{
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Apply formatter
+ *
+ * @access public
+ * @return mixed
+ */
+ public function format();
+}
diff --git a/app/Core/Filter/Lexer.php b/app/Core/Filter/Lexer.php
new file mode 100644
index 00000000..041b58d3
--- /dev/null
+++ b/app/Core/Filter/Lexer.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+/**
+ * Lexer
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class Lexer
+{
+ /**
+ * Current position
+ *
+ * @access private
+ * @var integer
+ */
+ private $offset = 0;
+
+ /**
+ * Token map
+ *
+ * @access private
+ * @var array
+ */
+ private $tokenMap = array(
+ "/^(\s+)/" => 'T_WHITESPACE',
+ '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
+ '/^(yesterday|tomorrow|today)/' => 'T_DATE',
+ '/^("(.*?)")/' => 'T_STRING',
+ "/^(\w+)/" => 'T_STRING',
+ "/^(#\d+)/" => 'T_STRING',
+ );
+
+ /**
+ * Default token
+ *
+ * @access private
+ * @var string
+ */
+ private $defaultToken = '';
+
+ /**
+ * Add token
+ *
+ * @access public
+ * @param string $regex
+ * @param string $token
+ * @return $this
+ */
+ public function addToken($regex, $token)
+ {
+ $this->tokenMap = array($regex => $token) + $this->tokenMap;
+ return $this;
+ }
+
+ /**
+ * Set default token
+ *
+ * @access public
+ * @param string $token
+ * @return $this
+ */
+ public function setDefaultToken($token)
+ {
+ $this->defaultToken = $token;
+ return $this;
+ }
+
+ /**
+ * Tokenize input string
+ *
+ * @access public
+ * @param string $input
+ * @return array
+ */
+ public function tokenize($input)
+ {
+ $tokens = array();
+ $this->offset = 0;
+
+ while (isset($input[$this->offset])) {
+ $result = $this->match(substr($input, $this->offset));
+
+ if ($result === false) {
+ return array();
+ }
+
+ $tokens[] = $result;
+ }
+
+ return $this->map($tokens);
+ }
+
+ /**
+ * Find a token that match and move the offset
+ *
+ * @access protected
+ * @param string $string
+ * @return array|boolean
+ */
+ protected function match($string)
+ {
+ foreach ($this->tokenMap as $pattern => $name) {
+ if (preg_match($pattern, $string, $matches)) {
+ $this->offset += strlen($matches[1]);
+
+ return array(
+ 'match' => trim($matches[1], '"'),
+ 'token' => $name,
+ );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Build map of tokens and matches
+ *
+ * @access protected
+ * @param array $tokens
+ * @return array
+ */
+ protected function map(array $tokens)
+ {
+ $map = array();
+ $leftOver = '';
+
+ while (false !== ($token = current($tokens))) {
+ if ($token['token'] === 'T_STRING' || $token['token'] === 'T_WHITESPACE') {
+ $leftOver .= $token['match'];
+ } else {
+ $next = next($tokens);
+
+ if ($next !== false && in_array($next['token'], array('T_STRING', 'T_DATE'))) {
+ $map[$token['token']][] = $next['match'];
+ }
+ }
+
+ next($tokens);
+ }
+
+ $leftOver = trim($leftOver);
+
+ if ($this->defaultToken !== '' && $leftOver !== '') {
+ $map[$this->defaultToken] = array($leftOver);
+ }
+
+ return $map;
+ }
+}
diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php
new file mode 100644
index 00000000..7a9a714f
--- /dev/null
+++ b/app/Core/Filter/LexerBuilder.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Lexer Builder
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class LexerBuilder
+{
+ /**
+ * Lexer object
+ *
+ * @access protected
+ * @var Lexer
+ */
+ protected $lexer;
+
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * List of filters
+ *
+ * @access protected
+ * @var FilterInterface[]
+ */
+ protected $filters;
+
+ /**
+ * QueryBuilder object
+ *
+ * @access protected
+ * @var QueryBuilder
+ */
+ protected $queryBuilder;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ */
+ public function __construct()
+ {
+ $this->lexer = new Lexer;
+ $this->queryBuilder = new QueryBuilder();
+ }
+
+ /**
+ * Add a filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @param bool $default
+ * @return LexerBuilder
+ */
+ public function withFilter(FilterInterface $filter, $default = false)
+ {
+ $attributes = $filter->getAttributes();
+
+ foreach ($attributes as $attribute) {
+ $this->filters[$attribute] = $filter;
+ $this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute);
+
+ if ($default) {
+ $this->lexer->setDefaultToken($attribute);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the query
+ *
+ * @access public
+ * @param Table $query
+ * @return LexerBuilder
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ $this->queryBuilder->withQuery($this->query);
+ return $this;
+ }
+
+ /**
+ * Parse the input and build the query
+ *
+ * @access public
+ * @param string $input
+ * @return QueryBuilder
+ */
+ public function build($input)
+ {
+ $tokens = $this->lexer->tokenize($input);
+
+ foreach ($tokens as $token => $values) {
+ if (isset($this->filters[$token])) {
+ $this->applyFilters($this->filters[$token], $values);
+ }
+ }
+
+ return $this->queryBuilder;
+ }
+
+ /**
+ * Apply filters to the query
+ *
+ * @access protected
+ * @param FilterInterface $filter
+ * @param array $values
+ */
+ protected function applyFilters(FilterInterface $filter, array $values)
+ {
+ $len = count($values);
+
+ if ($len > 1) {
+ $criteria = new OrCriteria();
+ $criteria->withQuery($this->query);
+
+ foreach ($values as $value) {
+ $currentFilter = clone($filter);
+ $criteria->withFilter($currentFilter->withValue($value));
+ }
+
+ $this->queryBuilder->withCriteria($criteria);
+ } elseif ($len === 1) {
+ $this->queryBuilder->withFilter($filter->withValue($values[0]));
+ }
+ }
+
+ /**
+ * Clone object with deep copy
+ */
+ public function __clone()
+ {
+ $this->lexer = clone $this->lexer;
+ $this->query = clone $this->query;
+ $this->queryBuilder = clone $this->queryBuilder;
+ }
+}
diff --git a/app/Core/Filter/OrCriteria.php b/app/Core/Filter/OrCriteria.php
new file mode 100644
index 00000000..174b8458
--- /dev/null
+++ b/app/Core/Filter/OrCriteria.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * OR criteria
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class OrCriteria implements CriteriaInterface
+{
+ /**
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * @var FilterInterface[]
+ */
+ protected $filters = array();
+
+ /**
+ * Set the Query
+ *
+ * @access public
+ * @param Table $query
+ * @return CriteriaInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return CriteriaInterface
+ */
+ public function withFilter(FilterInterface $filter)
+ {
+ $this->filters[] = $filter;
+ return $this;
+ }
+
+ /**
+ * Apply condition
+ *
+ * @access public
+ * @return CriteriaInterface
+ */
+ public function apply()
+ {
+ $this->query->beginOr();
+
+ foreach ($this->filters as $filter) {
+ $filter->withQuery($this->query)->apply();
+ }
+
+ $this->query->closeOr();
+ return $this;
+ }
+}
diff --git a/app/Core/Filter/QueryBuilder.php b/app/Core/Filter/QueryBuilder.php
new file mode 100644
index 00000000..3de82b63
--- /dev/null
+++ b/app/Core/Filter/QueryBuilder.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Class QueryBuilder
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class QueryBuilder
+{
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * Set the query
+ *
+ * @access public
+ * @param Table $query
+ * @return QueryBuilder
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set a filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return QueryBuilder
+ */
+ public function withFilter(FilterInterface $filter)
+ {
+ $filter->withQuery($this->query)->apply();
+ return $this;
+ }
+
+ /**
+ * Set a criteria
+ *
+ * @access public
+ * @param CriteriaInterface $criteria
+ * @return QueryBuilder
+ */
+ public function withCriteria(CriteriaInterface $criteria)
+ {
+ $criteria->withQuery($this->query)->apply();
+ return $this;
+ }
+
+ /**
+ * Set a formatter
+ *
+ * @access public
+ * @param FormatterInterface $formatter
+ * @return string|array
+ */
+ public function format(FormatterInterface $formatter)
+ {
+ return $formatter->withQuery($this->query)->format();
+ }
+
+ /**
+ * Get the query result as array
+ *
+ * @access public
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->query->findAll();
+ }
+
+ /**
+ * Get Query object
+ *
+ * @access public
+ * @return Table
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Clone object with deep copy
+ */
+ public function __clone()
+ {
+ $this->query = clone $this->query;
+ }
+}
diff --git a/app/Core/Helper.php b/app/Core/Helper.php
index 3764a67c..66f8d429 100644
--- a/app/Core/Helper.php
+++ b/app/Core/Helper.php
@@ -10,18 +10,23 @@ use Pimple\Container;
* @package core
* @author Frederic Guillot
*
- * @property \Kanboard\Helper\AppHelper $app
- * @property \Kanboard\Helper\AssetHelper $asset
- * @property \Kanboard\Helper\DateHelper $dt
- * @property \Kanboard\Helper\FileHelper $file
- * @property \Kanboard\Helper\FormHelper $form
- * @property \Kanboard\Helper\ModelHelper $model
- * @property \Kanboard\Helper\SubtaskHelper $subtask
- * @property \Kanboard\Helper\TaskHelper $task
- * @property \Kanboard\Helper\TextHelper $text
- * @property \Kanboard\Helper\UrlHelper $url
- * @property \Kanboard\Helper\UserHelper $user
- * @property \Kanboard\Helper\LayoutHelper $layout
+ * @property \Kanboard\Helper\AppHelper $app
+ * @property \Kanboard\Helper\AssetHelper $asset
+ * @property \Kanboard\Helper\CalendarHelper $calendar
+ * @property \Kanboard\Helper\DateHelper $dt
+ * @property \Kanboard\Helper\FileHelper $file
+ * @property \Kanboard\Helper\FormHelper $form
+ * @property \Kanboard\Helper\HookHelper $hook
+ * @property \Kanboard\Helper\ICalHelper $ical
+ * @property \Kanboard\Helper\ModelHelper $model
+ * @property \Kanboard\Helper\SubtaskHelper $subtask
+ * @property \Kanboard\Helper\TaskHelper $task
+ * @property \Kanboard\Helper\TextHelper $text
+ * @property \Kanboard\Helper\UrlHelper $url
+ * @property \Kanboard\Helper\UserHelper $user
+ * @property \Kanboard\Helper\LayoutHelper $layout
+ * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader
+ * @property \Kanboard\Helper\ProjectActivityHelper $projectActivity
*/
class Helper
{
diff --git a/app/Core/Http/OAuth2.php b/app/Core/Http/OAuth2.php
index 6fa1fb0a..211ca5b4 100644
--- a/app/Core/Http/OAuth2.php
+++ b/app/Core/Http/OAuth2.php
@@ -12,14 +12,14 @@ use Kanboard\Core\Base;
*/
class OAuth2 extends Base
{
- private $clientId;
- private $secret;
- private $callbackUrl;
- private $authUrl;
- private $tokenUrl;
- private $scopes;
- private $tokenType;
- private $accessToken;
+ protected $clientId;
+ protected $secret;
+ protected $callbackUrl;
+ protected $authUrl;
+ protected $tokenUrl;
+ protected $scopes;
+ protected $tokenType;
+ protected $accessToken;
/**
* Create OAuth2 service
@@ -46,6 +46,33 @@ class OAuth2 extends Base
}
/**
+ * Generate OAuth2 state and return the token value
+ *
+ * @access public
+ * @return string
+ */
+ public function getState()
+ {
+ if (! isset($this->sessionStorage->oauthState) || empty($this->sessionStorage->oauthState)) {
+ $this->sessionStorage->oauthState = $this->token->getToken();
+ }
+
+ return $this->sessionStorage->oauthState;
+ }
+
+ /**
+ * Check the validity of the state (CSRF token)
+ *
+ * @access public
+ * @param string $state
+ * @return bool
+ */
+ public function isValidateState($state)
+ {
+ return $state === $this->getState();
+ }
+
+ /**
* Get authorization url
*
* @access public
@@ -58,6 +85,7 @@ class OAuth2 extends Base
'client_id' => $this->clientId,
'redirect_uri' => $this->callbackUrl,
'scope' => implode(' ', $this->scopes),
+ 'state' => $this->getState(),
);
return $this->authUrl.'?'.http_build_query($params);
@@ -94,6 +122,7 @@ class OAuth2 extends Base
'client_secret' => $this->secret,
'redirect_uri' => $this->callbackUrl,
'grant_type' => 'authorization_code',
+ 'state' => $this->getState(),
);
$response = json_decode($this->httpClient->postForm($this->tokenUrl, $params, array('Accept: application/json')), true);
diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php
index d098f519..996fc58d 100644
--- a/app/Core/Http/Response.php
+++ b/app/Core/Http/Response.php
@@ -14,6 +14,24 @@ use Kanboard\Core\Csv;
class Response extends Base
{
/**
+ * Send headers to cache a resource
+ *
+ * @access public
+ * @param integer $duration
+ * @param string $etag
+ */
+ public function cache($duration, $etag = '')
+ {
+ header('Pragma: cache');
+ header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT');
+ header('Cache-Control: public, max-age=' . $duration);
+
+ if ($etag) {
+ header('ETag: "' . $etag . '"');
+ }
+ }
+
+ /**
* Send no cache headers
*
* @access public
@@ -214,6 +232,20 @@ class Response extends Base
}
/**
+ * Send a iCal response
+ *
+ * @access public
+ * @param string $data Raw data
+ * @param integer $status_code HTTP status code
+ */
+ public function ical($data, $status_code = 200)
+ {
+ $this->status($status_code);
+ $this->contentType('text/calendar; charset=utf-8');
+ echo $data;
+ }
+
+ /**
* Send the security header: Content-Security-Policy
*
* @access public
diff --git a/app/Core/Ldap/Client.php b/app/Core/Ldap/Client.php
index 05658190..867d67fe 100644
--- a/app/Core/Ldap/Client.php
+++ b/app/Core/Ldap/Client.php
@@ -3,6 +3,7 @@
namespace Kanboard\Core\Ldap;
use LogicException;
+use Psr\Log\LoggerInterface;
/**
* LDAP Client
@@ -21,6 +22,14 @@ class Client
protected $ldap;
/**
+ * Logger instance
+ *
+ * @access private
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
* Establish LDAP connection
*
* @static
@@ -165,4 +174,39 @@ class Client
{
return LDAP_PASSWORD;
}
+
+ /**
+ * Set logger
+ *
+ * @access public
+ * @param LoggerInterface $logger
+ * @return Client
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ return $this;
+ }
+
+ /**
+ * Get logger
+ *
+ * @access public
+ * @return LoggerInterface
+ */
+ public function getLogger()
+ {
+ return $this->logger;
+ }
+
+ /**
+ * Test if a logger is defined
+ *
+ * @access public
+ * @return boolean
+ */
+ public function hasLogger()
+ {
+ return $this->logger !== null;
+ }
}
diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php
index 1779fa61..7c1524ca 100644
--- a/app/Core/Ldap/Query.php
+++ b/app/Core/Ldap/Query.php
@@ -48,6 +48,12 @@ class Query
*/
public function execute($baseDn, $filter, array $attributes)
{
+ if (DEBUG && $this->client->hasLogger()) {
+ $this->client->getLogger()->debug('BaseDN='.$baseDn);
+ $this->client->getLogger()->debug('Filter='.$filter);
+ $this->client->getLogger()->debug('Attributes='.implode(', ', $attributes));
+ }
+
$sr = ldap_search($this->client->getConnection(), $baseDn, $filter, $attributes);
if ($sr === false) {
return $this;
diff --git a/app/Core/Ldap/User.php b/app/Core/Ldap/User.php
index 52283434..d23ec07e 100644
--- a/app/Core/Ldap/User.php
+++ b/app/Core/Ldap/User.php
@@ -44,8 +44,7 @@ class User
*/
public static function getUser(Client $client, $username)
{
- $className = get_called_class();
- $self = new $className(new Query($client));
+ $self = new static(new Query($client));
return $self->find($self->getLdapUserPattern($username));
}
diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php
deleted file mode 100644
index df2d90ae..00000000
--- a/app/Core/Lexer.php
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-
-namespace Kanboard\Core;
-
-/**
- * Lexer
- *
- * @package core
- * @author Frederic Guillot
- */
-class Lexer
-{
- /**
- * Current position
- *
- * @access private
- * @var integer
- */
- private $offset = 0;
-
- /**
- * Token map
- *
- * @access private
- * @var array
- */
- private $tokenMap = array(
- "/^(assignee:)/" => 'T_ASSIGNEE',
- "/^(color:)/" => 'T_COLOR',
- "/^(due:)/" => 'T_DUE',
- "/^(updated:)/" => 'T_UPDATED',
- "/^(modified:)/" => 'T_UPDATED',
- "/^(created:)/" => 'T_CREATED',
- "/^(status:)/" => 'T_STATUS',
- "/^(description:)/" => 'T_DESCRIPTION',
- "/^(category:)/" => 'T_CATEGORY',
- "/^(column:)/" => 'T_COLUMN',
- "/^(project:)/" => 'T_PROJECT',
- "/^(swimlane:)/" => 'T_SWIMLANE',
- "/^(ref:)/" => 'T_REFERENCE',
- "/^(reference:)/" => 'T_REFERENCE',
- "/^(link:)/" => 'T_LINK',
- "/^(\s+)/" => 'T_WHITESPACE',
- '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
- '/^(yesterday|tomorrow|today)/' => 'T_DATE',
- '/^("(.*?)")/' => 'T_STRING',
- "/^(\w+)/" => 'T_STRING',
- "/^(#\d+)/" => 'T_STRING',
- );
-
- /**
- * Tokenize input string
- *
- * @access public
- * @param string $input
- * @return array
- */
- public function tokenize($input)
- {
- $tokens = array();
- $this->offset = 0;
-
- while (isset($input[$this->offset])) {
- $result = $this->match(substr($input, $this->offset));
-
- if ($result === false) {
- return array();
- }
-
- $tokens[] = $result;
- }
-
- return $tokens;
- }
-
- /**
- * Find a token that match and move the offset
- *
- * @access public
- * @param string $string
- * @return array|boolean
- */
- public function match($string)
- {
- foreach ($this->tokenMap as $pattern => $name) {
- if (preg_match($pattern, $string, $matches)) {
- $this->offset += strlen($matches[1]);
-
- return array(
- 'match' => trim($matches[1], '"'),
- 'token' => $name,
- );
- }
- }
-
- return false;
- }
-
- /**
- * Change the output of tokenizer to be easily parsed by the database filter
- *
- * Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title']
- *
- * @access public
- * @param array $tokens
- * @return array
- */
- public function map(array $tokens)
- {
- $map = array(
- 'T_TITLE' => '',
- );
-
- while (false !== ($token = current($tokens))) {
- switch ($token['token']) {
- case 'T_ASSIGNEE':
- case 'T_COLOR':
- case 'T_CATEGORY':
- case 'T_COLUMN':
- case 'T_PROJECT':
- case 'T_SWIMLANE':
- case 'T_LINK':
- $next = next($tokens);
-
- if ($next !== false && $next['token'] === 'T_STRING') {
- $map[$token['token']][] = $next['match'];
- }
-
- break;
-
- case 'T_STATUS':
- case 'T_DUE':
- case 'T_UPDATED':
- case 'T_CREATED':
- case 'T_DESCRIPTION':
- case 'T_REFERENCE':
- $next = next($tokens);
-
- if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) {
- $map[$token['token']] = $next['match'];
- }
-
- break;
-
- default:
- $map['T_TITLE'] .= $token['match'];
- break;
- }
-
- next($tokens);
- }
-
- $map['T_TITLE'] = trim($map['T_TITLE']);
-
- if (empty($map['T_TITLE'])) {
- unset($map['T_TITLE']);
- }
-
- return $map;
- }
-}
diff --git a/app/Core/Session/SessionStorage.php b/app/Core/Session/SessionStorage.php
index 667d9253..6e2f9660 100644
--- a/app/Core/Session/SessionStorage.php
+++ b/app/Core/Session/SessionStorage.php
@@ -21,6 +21,7 @@ namespace Kanboard\Core\Session;
* @property bool $boardCollapsed
* @property bool $twoFactorBeforeCodeCalled
* @property string $twoFactorSecret
+ * @property string $oauthState
*/
class SessionStorage
{
diff --git a/app/Core/Template.php b/app/Core/Template.php
index f85c7f28..1874d44a 100644
--- a/app/Core/Template.php
+++ b/app/Core/Template.php
@@ -7,6 +7,21 @@ namespace Kanboard\Core;
*
* @package core
* @author Frederic Guillot
+ *
+ * @property \Kanboard\Helper\AppHelper $app
+ * @property \Kanboard\Helper\AssetHelper $asset
+ * @property \Kanboard\Helper\DateHelper $dt
+ * @property \Kanboard\Helper\FileHelper $file
+ * @property \Kanboard\Helper\FormHelper $form
+ * @property \Kanboard\Helper\HookHelper $hook
+ * @property \Kanboard\Helper\ModelHelper $model
+ * @property \Kanboard\Helper\SubtaskHelper $subtask
+ * @property \Kanboard\Helper\TaskHelper $task
+ * @property \Kanboard\Helper\TextHelper $text
+ * @property \Kanboard\Helper\UrlHelper $url
+ * @property \Kanboard\Helper\UserHelper $user
+ * @property \Kanboard\Helper\LayoutHelper $layout
+ * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader
*/
class Template
{
@@ -84,25 +99,26 @@ class Template
/**
* Find template filename
*
- * Core template name: 'task/show'
- * Plugin template name: 'myplugin:task/show'
+ * Core template: 'task/show' or 'kanboard:task/show'
+ * Plugin template: 'myplugin:task/show'
*
* @access public
- * @param string $template_name
+ * @param string $template
* @return string
*/
- public function getTemplateFile($template_name)
+ public function getTemplateFile($template)
{
- $template_name = isset($this->overrides[$template_name]) ? $this->overrides[$template_name] : $template_name;
+ $plugin = '';
+ $template = isset($this->overrides[$template]) ? $this->overrides[$template] : $template;
+
+ if (strpos($template, ':') !== false) {
+ list($plugin, $template) = explode(':', $template);
+ }
- if (strpos($template_name, ':') !== false) {
- list($plugin, $template) = explode(':', $template_name);
- $path = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins';
- $path .= DIRECTORY_SEPARATOR.ucfirst($plugin).DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.$template.'.php';
- } else {
- $path = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.$template_name.'.php';
+ if ($plugin !== 'kanboard' && $plugin !== '') {
+ return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', '..', 'plugins', ucfirst($plugin), 'Template', $template.'.php'));
}
- return $path;
+ return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Template', $template.'.php'));
}
}
diff --git a/app/Core/Thumbnail.php b/app/Core/Thumbnail.php
new file mode 100644
index 00000000..733d3a3c
--- /dev/null
+++ b/app/Core/Thumbnail.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Kanboard\Core;
+
+/**
+ * Thumbnail Generator
+ *
+ * @package core
+ * @author Frederic Guillot
+ */
+class Thumbnail
+{
+ protected $metadata = array();
+ protected $srcImage;
+ protected $dstImage;
+
+ /**
+ * Create a thumbnail from a local file
+ *
+ * @static
+ * @access public
+ * @param string $filename
+ * @return Thumbnail
+ */
+ public static function createFromFile($filename)
+ {
+ $self = new static();
+ $self->fromFile($filename);
+ return $self;
+ }
+
+ /**
+ * Create a thumbnail from a string
+ *
+ * @static
+ * @access public
+ * @param string $blob
+ * @return Thumbnail
+ */
+ public static function createFromString($blob)
+ {
+ $self = new static();
+ $self->fromString($blob);
+ return $self;
+ }
+
+ /**
+ * Load the local image file in memory with GD
+ *
+ * @access public
+ * @param string $filename
+ * @return Thumbnail
+ */
+ public function fromFile($filename)
+ {
+ $this->metadata = getimagesize($filename);
+ $this->srcImage = imagecreatefromstring(file_get_contents($filename));
+ return $this;
+ }
+
+ /**
+ * Load the image blob in memory with GD
+ *
+ * @access public
+ * @param string $blob
+ * @return Thumbnail
+ */
+ public function fromString($blob)
+ {
+ if (!function_exists('getimagesizefromstring')) {
+ $uri = 'data://application/octet-stream;base64,' . base64_encode($blob);
+ $this->metadata = getimagesize($uri);
+ } else {
+ $this->metadata = getimagesizefromstring($blob);
+ }
+
+ $this->srcImage = imagecreatefromstring($blob);
+ return $this;
+ }
+
+ /**
+ * Resize the image
+ *
+ * @access public
+ * @param int $width
+ * @param int $height
+ * @return Thumbnail
+ */
+ public function resize($width = 250, $height = 100)
+ {
+ $srcWidth = $this->metadata[0];
+ $srcHeight = $this->metadata[1];
+ $dstX = 0;
+ $dstY = 0;
+
+ if ($width == 0 && $height == 0) {
+ $width = 100;
+ $height = 100;
+ }
+
+ if ($width > 0 && $height == 0) {
+ $dstWidth = $width;
+ $dstHeight = floor($srcHeight * ($width / $srcWidth));
+ $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
+ } elseif ($width == 0 && $height > 0) {
+ $dstWidth = floor($srcWidth * ($height / $srcHeight));
+ $dstHeight = $height;
+ $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight);
+ } else {
+ $srcRatio = $srcWidth / $srcHeight;
+ $resizeRatio = $width / $height;
+
+ if ($srcRatio <= $resizeRatio) {
+ $dstWidth = $width;
+ $dstHeight = floor($srcHeight * ($width / $srcWidth));
+ $dstY = ($dstHeight - $height) / 2 * (-1);
+ } else {
+ $dstWidth = floor($srcWidth * ($height / $srcHeight));
+ $dstHeight = $height;
+ $dstX = ($dstWidth - $width) / 2 * (-1);
+ }
+
+ $this->dstImage = imagecreatetruecolor($width, $height);
+ }
+
+ imagecopyresampled($this->dstImage, $this->srcImage, $dstX, $dstY, 0, 0, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
+
+ return $this;
+ }
+
+ /**
+ * Save the thumbnail to a local file
+ *
+ * @access public
+ * @param string $filename
+ * @return Thumbnail
+ */
+ public function toFile($filename)
+ {
+ imagejpeg($this->dstImage, $filename);
+ imagedestroy($this->dstImage);
+ imagedestroy($this->srcImage);
+ return $this;
+ }
+
+ /**
+ * Return the thumbnail as a string
+ *
+ * @access public
+ * @return string
+ */
+ public function toString()
+ {
+ ob_start();
+ imagejpeg($this->dstImage, null);
+ imagedestroy($this->dstImage);
+ imagedestroy($this->srcImage);
+ return ob_get_clean();
+ }
+
+ /**
+ * Output the thumbnail directly to the browser or stdout
+ *
+ * @access public
+ */
+ public function toOutput()
+ {
+ imagejpeg($this->dstImage, null);
+ imagedestroy($this->dstImage);
+ imagedestroy($this->srcImage);
+ }
+}
diff --git a/app/Core/Tool.php b/app/Core/Tool.php
index db2445a1..3423998d 100644
--- a/app/Core/Tool.php
+++ b/app/Core/Tool.php
@@ -75,78 +75,4 @@ class Tool
return $container;
}
-
- /**
- * Generate a jpeg thumbnail from an image
- *
- * @static
- * @access public
- * @param string $src_file Source file image
- * @param string $dst_file Destination file image
- * @param integer $resize_width Desired image width
- * @param integer $resize_height Desired image height
- */
- public static function generateThumbnail($src_file, $dst_file, $resize_width = 250, $resize_height = 100)
- {
- $metadata = getimagesize($src_file);
- $src_width = $metadata[0];
- $src_height = $metadata[1];
- $dst_y = 0;
- $dst_x = 0;
-
- if (empty($metadata['mime'])) {
- return;
- }
-
- if ($resize_width == 0 && $resize_height == 0) {
- $resize_width = 100;
- $resize_height = 100;
- }
-
- if ($resize_width > 0 && $resize_height == 0) {
- $dst_width = $resize_width;
- $dst_height = floor($src_height * ($resize_width / $src_width));
- $dst_image = imagecreatetruecolor($dst_width, $dst_height);
- } elseif ($resize_width == 0 && $resize_height > 0) {
- $dst_width = floor($src_width * ($resize_height / $src_height));
- $dst_height = $resize_height;
- $dst_image = imagecreatetruecolor($dst_width, $dst_height);
- } else {
- $src_ratio = $src_width / $src_height;
- $resize_ratio = $resize_width / $resize_height;
-
- if ($src_ratio <= $resize_ratio) {
- $dst_width = $resize_width;
- $dst_height = floor($src_height * ($resize_width / $src_width));
-
- $dst_y = ($dst_height - $resize_height) / 2 * (-1);
- } else {
- $dst_width = floor($src_width * ($resize_height / $src_height));
- $dst_height = $resize_height;
-
- $dst_x = ($dst_width - $resize_width) / 2 * (-1);
- }
-
- $dst_image = imagecreatetruecolor($resize_width, $resize_height);
- }
-
- switch ($metadata['mime']) {
- case 'image/jpeg':
- case 'image/jpg':
- $src_image = imagecreatefromjpeg($src_file);
- break;
- case 'image/png':
- $src_image = imagecreatefrompng($src_file);
- break;
- case 'image/gif':
- $src_image = imagecreatefromgif($src_file);
- break;
- default:
- return;
- }
-
- imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height);
- imagejpeg($dst_image, $dst_file);
- imagedestroy($dst_image);
- }
}
diff --git a/app/Core/User/Avatar/AvatarManager.php b/app/Core/User/Avatar/AvatarManager.php
index 71bd8aa5..5b61cbdb 100644
--- a/app/Core/User/Avatar/AvatarManager.php
+++ b/app/Core/User/Avatar/AvatarManager.php
@@ -32,23 +32,25 @@ class AvatarManager
}
/**
- * Render avatar html element
+ * Render avatar HTML element
*
* @access public
* @param string $user_id
* @param string $username
* @param string $name
* @param string $email
+ * @param string $avatar_path
* @param int $size
* @return string
*/
- public function render($user_id, $username, $name, $email, $size)
+ public function render($user_id, $username, $name, $email, $avatar_path, $size)
{
$user = array(
'id' => $user_id,
'username' => $username,
'name' => $name,
'email' => $email,
+ 'avatar_path' => $avatar_path,
);
krsort($this->providers);
@@ -80,6 +82,7 @@ class AvatarManager
'username' => '',
'name' => '?',
'email' => '',
+ 'avatar_path' => '',
);
return $provider->render($user, $size);
diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php
index e494e7b4..0034c47a 100644
--- a/app/Core/User/UserSession.php
+++ b/app/Core/User/UserSession.php
@@ -14,6 +14,19 @@ use Kanboard\Core\Security\Role;
class UserSession extends Base
{
/**
+ * Refresh current session if necessary
+ *
+ * @access public
+ * @param integer $user_id
+ */
+ public function refresh($user_id)
+ {
+ if ($this->getId() == $user_id) {
+ $this->initialize($this->user->getById($user_id));
+ }
+ }
+
+ /**
* Update user session
*
* @access public
diff --git a/app/Filter/BaseDateFilter.php b/app/Filter/BaseDateFilter.php
new file mode 100644
index 00000000..56fb2d78
--- /dev/null
+++ b/app/Filter/BaseDateFilter.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\DateParser;
+
+/**
+ * Base date filter class
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+abstract class BaseDateFilter extends BaseFilter
+{
+ /**
+ * DateParser object
+ *
+ * @access protected
+ * @var DateParser
+ */
+ protected $dateParser;
+
+ /**
+ * Set DateParser object
+ *
+ * @access public
+ * @param DateParser $dateParser
+ * @return $this
+ */
+ public function setDateParser(DateParser $dateParser)
+ {
+ $this->dateParser = $dateParser;
+ return $this;
+ }
+
+ /**
+ * Parse operator in the input string
+ *
+ * @access protected
+ * @return string
+ */
+ protected function parseOperator()
+ {
+ $operators = array(
+ '<=' => 'lte',
+ '>=' => 'gte',
+ '<' => 'lt',
+ '>' => 'gt',
+ );
+
+ foreach ($operators as $operator => $method) {
+ if (strpos($this->value, $operator) === 0) {
+ $this->value = substr($this->value, strlen($operator));
+ return $method;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Apply a date filter
+ *
+ * @access protected
+ * @param string $field
+ */
+ protected function applyDateFilter($field)
+ {
+ $method = $this->parseOperator();
+ $timestamp = $this->dateParser->getTimestampFromIsoFormat($this->value);
+
+ if ($method !== '') {
+ $this->query->$method($field, $this->getTimestampFromOperator($method, $timestamp));
+ } else {
+ $this->query->gte($field, $timestamp);
+ $this->query->lte($field, $timestamp + 86399);
+ }
+ }
+
+ /**
+ * Get timestamp from the operator
+ *
+ * @access public
+ * @param string $method
+ * @param integer $timestamp
+ * @return integer
+ */
+ protected function getTimestampFromOperator($method, $timestamp)
+ {
+ switch ($method) {
+ case 'lte':
+ return $timestamp + 86399;
+ case 'lt':
+ return $timestamp;
+ case 'gte':
+ return $timestamp;
+ case 'gt':
+ return $timestamp + 86400;
+ }
+
+ return $timestamp;
+ }
+}
diff --git a/app/Filter/BaseFilter.php b/app/Filter/BaseFilter.php
new file mode 100644
index 00000000..79a664be
--- /dev/null
+++ b/app/Filter/BaseFilter.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Base filter class
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+abstract class BaseFilter
+{
+ /**
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * @var mixed
+ */
+ protected $value;
+
+ /**
+ * BaseFilter constructor
+ *
+ * @access public
+ * @param mixed $value
+ */
+ public function __construct($value = null)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Get object instance
+ *
+ * @static
+ * @access public
+ * @param mixed $value
+ * @return static
+ */
+ public static function getInstance($value = null)
+ {
+ $self = new static($value);
+ return $self;
+ }
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return \Kanboard\Core\Filter\FilterInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set the value
+ *
+ * @access public
+ * @param string $value
+ * @return \Kanboard\Core\Filter\FilterInterface
+ */
+ public function withValue($value)
+ {
+ $this->value = $value;
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityCreationDateFilter.php b/app/Filter/ProjectActivityCreationDateFilter.php
new file mode 100644
index 00000000..d0b7f754
--- /dev/null
+++ b/app/Filter/ProjectActivityCreationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by creation date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityCreationDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('created');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(ProjectActivity::TABLE.'.date_creation');
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityCreatorFilter.php b/app/Filter/ProjectActivityCreatorFilter.php
new file mode 100644
index 00000000..c95569d6
--- /dev/null
+++ b/app/Filter/ProjectActivityCreatorFilter.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by creator
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityCreatorFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('creator');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if ($this->value === 'me') {
+ $this->query->eq(ProjectActivity::TABLE . '.creator_id', $this->currentUserId);
+ } else {
+ $this->query->beginOr();
+ $this->query->ilike('uc.username', '%'.$this->value.'%');
+ $this->query->ilike('uc.name', '%'.$this->value.'%');
+ $this->query->closeOr();
+ }
+ }
+}
diff --git a/app/Filter/ProjectActivityProjectIdFilter.php b/app/Filter/ProjectActivityProjectIdFilter.php
new file mode 100644
index 00000000..bb4d8bd1
--- /dev/null
+++ b/app/Filter/ProjectActivityProjectIdFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by projectId
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityProjectIdFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project_id');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectActivity::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityProjectIdsFilter.php b/app/Filter/ProjectActivityProjectIdsFilter.php
new file mode 100644
index 00000000..47cf0c25
--- /dev/null
+++ b/app/Filter/ProjectActivityProjectIdsFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by projectIds
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityProjectIdsFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('projects');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (empty($this->value)) {
+ $this->query->eq(ProjectActivity::TABLE.'.project_id', 0);
+ } else {
+ $this->query->in(ProjectActivity::TABLE.'.project_id', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityProjectNameFilter.php b/app/Filter/ProjectActivityProjectNameFilter.php
new file mode 100644
index 00000000..0cf73657
--- /dev/null
+++ b/app/Filter/ProjectActivityProjectNameFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter activity events by project name
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityProjectNameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->ilike(Project::TABLE.'.name', '%'.$this->value.'%');
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityTaskIdFilter.php b/app/Filter/ProjectActivityTaskIdFilter.php
new file mode 100644
index 00000000..e99efe09
--- /dev/null
+++ b/app/Filter/ProjectActivityTaskIdFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Filter activity events by taskId
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityTaskIdFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('task_id');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectActivity::TABLE.'.task_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityTaskStatusFilter.php b/app/Filter/ProjectActivityTaskStatusFilter.php
new file mode 100644
index 00000000..69e2c52d
--- /dev/null
+++ b/app/Filter/ProjectActivityTaskStatusFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter activity events by task status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityTaskStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if ($this->value === 'open') {
+ $this->query->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN);
+ } elseif ($this->value === 'closed') {
+ $this->query->eq(Task::TABLE.'.is_active', Task::STATUS_CLOSED);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectActivityTaskTitleFilter.php b/app/Filter/ProjectActivityTaskTitleFilter.php
new file mode 100644
index 00000000..bf2afa30
--- /dev/null
+++ b/app/Filter/ProjectActivityTaskTitleFilter.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+
+/**
+ * Filter activity events by task title
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectActivityTaskTitleFilter extends TaskTitleFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('title');
+ }
+}
diff --git a/app/Filter/ProjectGroupRoleProjectFilter.php b/app/Filter/ProjectGroupRoleProjectFilter.php
new file mode 100644
index 00000000..b0950868
--- /dev/null
+++ b/app/Filter/ProjectGroupRoleProjectFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectGroupRole;
+
+/**
+ * Filter ProjectGroupRole users by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectGroupRoleProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectGroupRoleUsernameFilter.php b/app/Filter/ProjectGroupRoleUsernameFilter.php
new file mode 100644
index 00000000..c10855bc
--- /dev/null
+++ b/app/Filter/ProjectGroupRoleUsernameFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\GroupMember;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Model\User;
+
+/**
+ * Filter ProjectGroupRole users by username
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectGroupRoleUsernameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->ilike(User::TABLE.'.username', $this->value.'%');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectIdsFilter.php b/app/Filter/ProjectIdsFilter.php
new file mode 100644
index 00000000..641f7f18
--- /dev/null
+++ b/app/Filter/ProjectIdsFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectIdsFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project_ids');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (empty($this->value)) {
+ $this->query->eq(Project::TABLE.'.id', 0);
+ } else {
+ $this->query->in(Project::TABLE.'.id', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectStatusFilter.php b/app/Filter/ProjectStatusFilter.php
new file mode 100644
index 00000000..a994600c
--- /dev/null
+++ b/app/Filter/ProjectStatusFilter.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Project::TABLE.'.is_active', $this->value);
+ } elseif ($this->value === 'inactive' || $this->value === 'closed' || $this->value === 'disabled') {
+ $this->query->eq(Project::TABLE.'.is_active', 0);
+ } else {
+ $this->query->eq(Project::TABLE.'.is_active', 1);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectTypeFilter.php b/app/Filter/ProjectTypeFilter.php
new file mode 100644
index 00000000..e085e2f6
--- /dev/null
+++ b/app/Filter/ProjectTypeFilter.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by type
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectTypeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('type');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Project::TABLE.'.is_private', $this->value);
+ } elseif ($this->value === 'private') {
+ $this->query->eq(Project::TABLE.'.is_private', Project::TYPE_PRIVATE);
+ } else {
+ $this->query->eq(Project::TABLE.'.is_private', Project::TYPE_TEAM);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectUserRoleProjectFilter.php b/app/Filter/ProjectUserRoleProjectFilter.php
new file mode 100644
index 00000000..3b880df5
--- /dev/null
+++ b/app/Filter/ProjectUserRoleProjectFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectUserRole;
+
+/**
+ * Filter ProjectUserRole users by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectUserRoleProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectUserRole::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectUserRoleUsernameFilter.php b/app/Filter/ProjectUserRoleUsernameFilter.php
new file mode 100644
index 00000000..c00493a3
--- /dev/null
+++ b/app/Filter/ProjectUserRoleUsernameFilter.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\User;
+
+/**
+ * Filter ProjectUserRole users by username
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectUserRoleUsernameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query
+ ->join(User::TABLE, 'id', 'user_id')
+ ->ilike(User::TABLE.'.username', $this->value.'%');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskAssigneeFilter.php b/app/Filter/TaskAssigneeFilter.php
new file mode 100644
index 00000000..783d6a12
--- /dev/null
+++ b/app/Filter/TaskAssigneeFilter.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+use Kanboard\Model\User;
+
+/**
+ * Filter tasks by assignee
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskAssigneeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('assignee');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.owner_id', $this->value);
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $this->query->eq(Task::TABLE.'.owner_id', $this->currentUserId);
+ break;
+ case 'nobody':
+ $this->query->eq(Task::TABLE.'.owner_id', 0);
+ break;
+ default:
+ $this->query->beginOr();
+ $this->query->ilike(User::TABLE.'.username', '%'.$this->value.'%');
+ $this->query->ilike(User::TABLE.'.name', '%'.$this->value.'%');
+ $this->query->closeOr();
+ }
+ }
+ }
+}
diff --git a/app/Filter/TaskCategoryFilter.php b/app/Filter/TaskCategoryFilter.php
new file mode 100644
index 00000000..517f24d9
--- /dev/null
+++ b/app/Filter/TaskCategoryFilter.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Category;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by category
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCategoryFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('category');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.category_id', $this->value);
+ } elseif ($this->value === 'none') {
+ $this->query->eq(Task::TABLE.'.category_id', 0);
+ } else {
+ $this->query->eq(Category::TABLE.'.name', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskColorFilter.php b/app/Filter/TaskColorFilter.php
new file mode 100644
index 00000000..784162d4
--- /dev/null
+++ b/app/Filter/TaskColorFilter.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Color;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by color
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskColorFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Color object
+ *
+ * @access private
+ * @var Color
+ */
+ private $colorModel;
+
+ /**
+ * Set color model object
+ *
+ * @access public
+ * @param Color $colorModel
+ * @return TaskColorFilter
+ */
+ public function setColorModel(Color $colorModel)
+ {
+ $this->colorModel = $colorModel;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('color', 'colour');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.color_id', $this->colorModel->find($this->value));
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskColumnFilter.php b/app/Filter/TaskColumnFilter.php
new file mode 100644
index 00000000..9a4d4253
--- /dev/null
+++ b/app/Filter/TaskColumnFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Column;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by column
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskColumnFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('column');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.column_id', $this->value);
+ } else {
+ $this->query->eq(Column::TABLE.'.title', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCommentFilter.php b/app/Filter/TaskCommentFilter.php
new file mode 100644
index 00000000..455098c2
--- /dev/null
+++ b/app/Filter/TaskCommentFilter.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Comment;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by comment
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCommentFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('comment');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->ilike(Comment::TABLE.'.comment', '%'.$this->value.'%');
+ $this->query->join(Comment::TABLE, 'task_id', 'id', Task::TABLE);
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCompletionDateFilter.php b/app/Filter/TaskCompletionDateFilter.php
new file mode 100644
index 00000000..f206a3e2
--- /dev/null
+++ b/app/Filter/TaskCompletionDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by completion date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCompletionDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('completed');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_completed');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCreationDateFilter.php b/app/Filter/TaskCreationDateFilter.php
new file mode 100644
index 00000000..bb6efad6
--- /dev/null
+++ b/app/Filter/TaskCreationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by creation date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCreationDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('created');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_creation');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCreatorFilter.php b/app/Filter/TaskCreatorFilter.php
new file mode 100644
index 00000000..af35e6bc
--- /dev/null
+++ b/app/Filter/TaskCreatorFilter.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by creator
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCreatorFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('creator');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.creator_id', $this->value);
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $this->query->eq(Task::TABLE.'.creator_id', $this->currentUserId);
+ break;
+ case 'nobody':
+ $this->query->eq(Task::TABLE.'.creator_id', 0);
+ break;
+ default:
+ $this->query->beginOr();
+ $this->query->ilike('uc.username', '%'.$this->value.'%');
+ $this->query->ilike('uc.name', '%'.$this->value.'%');
+ $this->query->closeOr();
+ }
+ }
+ }
+}
diff --git a/app/Filter/TaskDescriptionFilter.php b/app/Filter/TaskDescriptionFilter.php
new file mode 100644
index 00000000..6dda58ae
--- /dev/null
+++ b/app/Filter/TaskDescriptionFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by description
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDescriptionFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('description', 'desc');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->ilike(Task::TABLE.'.description', '%'.$this->value.'%');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskDueDateFilter.php b/app/Filter/TaskDueDateFilter.php
new file mode 100644
index 00000000..e36efdd0
--- /dev/null
+++ b/app/Filter/TaskDueDateFilter.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by due date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDueDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('due');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->neq(Task::TABLE.'.date_due', 0);
+ $this->query->notNull(Task::TABLE.'.date_due');
+ $this->applyDateFilter(Task::TABLE.'.date_due');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskDueDateRangeFilter.php b/app/Filter/TaskDueDateRangeFilter.php
new file mode 100644
index 00000000..10deb0d3
--- /dev/null
+++ b/app/Filter/TaskDueDateRangeFilter.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by due date range
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDueDateRangeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->gte(Task::TABLE.'.date_due', is_numeric($this->value[0]) ? $this->value[0] : strtotime($this->value[0]));
+ $this->query->lte(Task::TABLE.'.date_due', is_numeric($this->value[1]) ? $this->value[1] : strtotime($this->value[1]));
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskIdExclusionFilter.php b/app/Filter/TaskIdExclusionFilter.php
new file mode 100644
index 00000000..8bfefb2b
--- /dev/null
+++ b/app/Filter/TaskIdExclusionFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Exclude task ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskIdExclusionFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('exclude');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->notin(Task::TABLE.'.id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskIdFilter.php b/app/Filter/TaskIdFilter.php
new file mode 100644
index 00000000..87bac794
--- /dev/null
+++ b/app/Filter/TaskIdFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by id
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskIdFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('id');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskLinkFilter.php b/app/Filter/TaskLinkFilter.php
new file mode 100644
index 00000000..18a13a09
--- /dev/null
+++ b/app/Filter/TaskLinkFilter.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Link;
+use Kanboard\Model\Task;
+use Kanboard\Model\TaskLink;
+use PicoDb\Database;
+use PicoDb\Table;
+
+/**
+ * Filter tasks by link name
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskLinkFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return TaskLinkFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('link');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ $task_ids = $this->getSubQuery()->findAllByColumn('task_id');
+
+ if (! empty($task_ids)) {
+ $this->query->in(Task::TABLE.'.id', $task_ids);
+ } else {
+ $this->query->eq(Task::TABLE.'.id', 0); // No match
+ }
+ }
+
+ /**
+ * Get subquery
+ *
+ * @access protected
+ * @return Table
+ */
+ protected function getSubQuery()
+ {
+ return $this->db->table(TaskLink::TABLE)
+ ->columns(
+ TaskLink::TABLE.'.task_id',
+ Link::TABLE.'.label'
+ )
+ ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE)
+ ->ilike(Link::TABLE.'.label', $this->value);
+ }
+}
diff --git a/app/Filter/TaskModificationDateFilter.php b/app/Filter/TaskModificationDateFilter.php
new file mode 100644
index 00000000..5036e9c1
--- /dev/null
+++ b/app/Filter/TaskModificationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by modification date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskModificationDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('updated', 'modified');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_modification');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskProjectFilter.php b/app/Filter/TaskProjectFilter.php
new file mode 100644
index 00000000..e432efee
--- /dev/null
+++ b/app/Filter/TaskProjectFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.project_id', $this->value);
+ } else {
+ $this->query->ilike(Project::TABLE.'.name', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskProjectsFilter.php b/app/Filter/TaskProjectsFilter.php
new file mode 100644
index 00000000..47636b1d
--- /dev/null
+++ b/app/Filter/TaskProjectsFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by project ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskProjectsFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('projects');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (empty($this->value)) {
+ $this->query->eq(Task::TABLE.'.project_id', 0);
+ } else {
+ $this->query->in(Task::TABLE.'.project_id', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskReferenceFilter.php b/app/Filter/TaskReferenceFilter.php
new file mode 100644
index 00000000..4ad47dd5
--- /dev/null
+++ b/app/Filter/TaskReferenceFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by reference
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskReferenceFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('reference', 'ref');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.reference', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskStartDateFilter.php b/app/Filter/TaskStartDateFilter.php
new file mode 100644
index 00000000..dd30762b
--- /dev/null
+++ b/app/Filter/TaskStartDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by start date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskStartDateFilter extends BaseDateFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('started');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_started');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskStatusFilter.php b/app/Filter/TaskStatusFilter.php
new file mode 100644
index 00000000..0ba4361e
--- /dev/null
+++ b/app/Filter/TaskStatusFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if ($this->value === 'open' || $this->value === 'closed') {
+ $this->query->eq(Task::TABLE.'.is_active', $this->value === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED);
+ } else {
+ $this->query->eq(Task::TABLE.'.is_active', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskSubtaskAssigneeFilter.php b/app/Filter/TaskSubtaskAssigneeFilter.php
new file mode 100644
index 00000000..4c757315
--- /dev/null
+++ b/app/Filter/TaskSubtaskAssigneeFilter.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Subtask;
+use Kanboard\Model\Task;
+use Kanboard\Model\User;
+use PicoDb\Database;
+use PicoDb\Table;
+
+/**
+ * Filter tasks by subtasks assignee
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskSubtaskAssigneeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskSubtaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return TaskSubtaskAssigneeFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('subtask:assignee');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ $task_ids = $this->getSubQuery()->findAllByColumn('task_id');
+
+ if (! empty($task_ids)) {
+ $this->query->in(Task::TABLE.'.id', $task_ids);
+ } else {
+ $this->query->eq(Task::TABLE.'.id', 0); // No match
+ }
+ }
+
+ /**
+ * Get subquery
+ *
+ * @access protected
+ * @return Table
+ */
+ protected function getSubQuery()
+ {
+ $subquery = $this->db->table(Subtask::TABLE)
+ ->columns(
+ Subtask::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ User::TABLE.'.name',
+ User::TABLE.'.username'
+ )
+ ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE)
+ ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE);
+
+ return $this->applySubQueryFilter($subquery);
+ }
+
+ /**
+ * Apply subquery filter
+ *
+ * @access protected
+ * @param Table $subquery
+ * @return Table
+ */
+ protected function applySubQueryFilter(Table $subquery)
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $subquery->eq(Subtask::TABLE.'.user_id', $this->value);
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $subquery->eq(Subtask::TABLE.'.user_id', $this->currentUserId);
+ break;
+ case 'nobody':
+ $subquery->eq(Subtask::TABLE.'.user_id', 0);
+ break;
+ default:
+ $subquery->beginOr();
+ $subquery->ilike(User::TABLE.'.username', $this->value.'%');
+ $subquery->ilike(User::TABLE.'.name', '%'.$this->value.'%');
+ $subquery->closeOr();
+ }
+ }
+
+ return $subquery;
+ }
+}
diff --git a/app/Filter/TaskSwimlaneFilter.php b/app/Filter/TaskSwimlaneFilter.php
new file mode 100644
index 00000000..4e030244
--- /dev/null
+++ b/app/Filter/TaskSwimlaneFilter.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+use Kanboard\Model\Swimlane;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by swimlane
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskSwimlaneFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('swimlane');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.swimlane_id', $this->value);
+ } elseif ($this->value === 'default') {
+ $this->query->eq(Task::TABLE.'.swimlane_id', 0);
+ } else {
+ $this->query->beginOr();
+ $this->query->ilike(Swimlane::TABLE.'.name', $this->value);
+ $this->query->ilike(Project::TABLE.'.default_swimlane', $this->value);
+ $this->query->closeOr();
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskTitleFilter.php b/app/Filter/TaskTitleFilter.php
new file mode 100644
index 00000000..9853369c
--- /dev/null
+++ b/app/Filter/TaskTitleFilter.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by title
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskTitleFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('title');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (ctype_digit($this->value) || (strlen($this->value) > 1 && $this->value{0} === '#' && ctype_digit(substr($this->value, 1)))) {
+ $this->query->beginOr();
+ $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $this->value));
+ $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%');
+ $this->query->closeOr();
+ } else {
+ $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%');
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/UserNameFilter.php b/app/Filter/UserNameFilter.php
new file mode 100644
index 00000000..dfb07fdd
--- /dev/null
+++ b/app/Filter/UserNameFilter.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+
+class UserNameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('name');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->beginOr()
+ ->ilike('username', '%'.$this->value.'%')
+ ->ilike('name', '%'.$this->value.'%')
+ ->closeOr();
+
+ return $this;
+ }
+}
diff --git a/app/Formatter/BaseFormatter.php b/app/Formatter/BaseFormatter.php
new file mode 100644
index 00000000..a9f0ad15
--- /dev/null
+++ b/app/Formatter/BaseFormatter.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\FormatterInterface;
+use PicoDb\Table;
+
+/**
+ * Class BaseFormatter
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+abstract class BaseFormatter extends Base
+{
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+}
diff --git a/app/Formatter/TaskFilterCalendarEvent.php b/app/Formatter/BaseTaskCalendarFormatter.php
index 12ea8687..8fab3e9a 100644
--- a/app/Formatter/TaskFilterCalendarEvent.php
+++ b/app/Formatter/BaseTaskCalendarFormatter.php
@@ -2,7 +2,7 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
* Common class to handle calendar events
@@ -10,7 +10,7 @@ use Kanboard\Model\TaskFilter;
* @package formatter
* @author Frederic Guillot
*/
-abstract class TaskFilterCalendarEvent extends TaskFilter
+abstract class BaseTaskCalendarFormatter extends BaseFormatter
{
/**
* Column used for event start date
@@ -29,20 +29,12 @@ abstract class TaskFilterCalendarEvent extends TaskFilter
protected $endColumn = 'date_completed';
/**
- * Full day event flag
- *
- * @access private
- * @var boolean
- */
- private $fullDay = false;
-
- /**
* Transform results to calendar events
*
* @access public
* @param string $start_column Column name for the start date
* @param string $end_column Column name for the end date
- * @return TaskFilterCalendarEvent
+ * @return FormatterInterface
*/
public function setColumns($start_column, $end_column = '')
{
@@ -50,27 +42,4 @@ abstract class TaskFilterCalendarEvent extends TaskFilter
$this->endColumn = $end_column ?: $start_column;
return $this;
}
-
- /**
- * When called calendar events will be full day
- *
- * @access public
- * @return TaskFilterCalendarEvent
- */
- public function setFullDay()
- {
- $this->fullDay = true;
- return $this;
- }
-
- /**
- * Return true if the events are full day
- *
- * @access public
- * @return boolean
- */
- public function isFullDay()
- {
- return $this->fullDay;
- }
}
diff --git a/app/Formatter/BoardFormatter.php b/app/Formatter/BoardFormatter.php
new file mode 100644
index 00000000..6a96b3e6
--- /dev/null
+++ b/app/Formatter/BoardFormatter.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Filter\FormatterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Board Formatter
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+class BoardFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Project id
+ *
+ * @access protected
+ * @var integer
+ */
+ protected $projectId;
+
+ /**
+ * Set ProjectId
+ *
+ * @access public
+ * @param integer $projectId
+ * @return $this
+ */
+ public function setProjectId($projectId)
+ {
+ $this->projectId = $projectId;
+ return $this;
+ }
+
+ /**
+ * Apply formatter
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $tasks = $this->query
+ ->eq(Task::TABLE.'.project_id', $this->projectId)
+ ->asc(Task::TABLE.'.position')
+ ->findAll();
+
+ return $this->board->getBoard($this->projectId, function ($project_id, $column_id, $swimlane_id) use ($tasks) {
+ return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) {
+ return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id;
+ });
+ });
+ }
+}
diff --git a/app/Formatter/FormatterInterface.php b/app/Formatter/FormatterInterface.php
deleted file mode 100644
index 0bb61292..00000000
--- a/app/Formatter/FormatterInterface.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Kanboard\Formatter;
-
-/**
- * Formatter Interface
- *
- * @package formatter
- * @author Frederic Guillot
- */
-interface FormatterInterface
-{
- public function format();
-}
diff --git a/app/Formatter/GroupAutoCompleteFormatter.php b/app/Formatter/GroupAutoCompleteFormatter.php
index 7023e367..4d552886 100644
--- a/app/Formatter/GroupAutoCompleteFormatter.php
+++ b/app/Formatter/GroupAutoCompleteFormatter.php
@@ -2,8 +2,12 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
+use Kanboard\Core\Group\GroupProviderInterface;
+use PicoDb\Table;
+
/**
- * Autocomplete formatter for groups
+ * Auto-complete formatter for groups
*
* @package formatter
* @author Frederic Guillot
@@ -14,25 +18,35 @@ class GroupAutoCompleteFormatter implements FormatterInterface
* Groups found
*
* @access private
- * @var array
+ * @var GroupProviderInterface[]
*/
private $groups;
/**
- * Format groups for the ajax autocompletion
+ * Format groups for the ajax auto-completion
*
* @access public
- * @param array $groups
- * @return GroupAutoCompleteFormatter
+ * @param GroupProviderInterface[] $groups
*/
- public function setGroups(array $groups)
+ public function __construct(array $groups)
{
$this->groups = $groups;
+ }
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query)
+ {
return $this;
}
/**
- * Format groups for the ajax autocompletion
+ * Format groups for the ajax auto-completion
*
* @access public
* @return array
diff --git a/app/Formatter/ProjectActivityEventFormatter.php b/app/Formatter/ProjectActivityEventFormatter.php
new file mode 100644
index 00000000..ae80e5e7
--- /dev/null
+++ b/app/Formatter/ProjectActivityEventFormatter.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Filter\FormatterInterface;
+
+class ProjectActivityEventFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Apply formatter
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $events = $this->query->findAll();
+
+ foreach ($events as &$event) {
+ $event += $this->unserializeEvent($event['data']);
+ unset($event['data']);
+
+ $event['author'] = $event['author_name'] ?: $event['author_username'];
+ $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event);
+ $event['event_content'] = $this->renderEvent($event);
+ }
+
+ return $events;
+ }
+
+ /**
+ * Decode event data, supports unserialize() and json_decode()
+ *
+ * @access protected
+ * @param string $data Serialized data
+ * @return array
+ */
+ protected function unserializeEvent($data)
+ {
+ if ($data{0} === 'a') {
+ return unserialize($data);
+ }
+
+ return json_decode($data, true) ?: array();
+ }
+
+ /**
+ * Get the event html content
+ *
+ * @access protected
+ * @param array $params Event properties
+ * @return string
+ */
+ protected function renderEvent(array $params)
+ {
+ return $this->template->render(
+ 'event/'.str_replace('.', '_', $params['event_name']),
+ $params
+ );
+ }
+}
diff --git a/app/Formatter/ProjectGanttFormatter.php b/app/Formatter/ProjectGanttFormatter.php
index 4f73e217..aee1f27f 100644
--- a/app/Formatter/ProjectGanttFormatter.php
+++ b/app/Formatter/ProjectGanttFormatter.php
@@ -2,7 +2,7 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\Project;
+use Kanboard\Core\Filter\FormatterInterface;
/**
* Gantt chart formatter for projects
@@ -10,41 +10,9 @@ use Kanboard\Model\Project;
* @package formatter
* @author Frederic Guillot
*/
-class ProjectGanttFormatter extends Project implements FormatterInterface
+class ProjectGanttFormatter extends BaseFormatter implements FormatterInterface
{
/**
- * List of projects
- *
- * @access private
- * @var array
- */
- private $projects = array();
-
- /**
- * Filter projects to generate the Gantt chart
- *
- * @access public
- * @param int[] $project_ids
- * @return ProjectGanttFormatter
- */
- public function filter(array $project_ids)
- {
- if (empty($project_ids)) {
- $this->projects = array();
- } else {
- $this->projects = $this->db
- ->table(self::TABLE)
- ->asc('start_date')
- ->in('id', $project_ids)
- ->eq('is_active', self::ACTIVE)
- ->eq('is_private', 0)
- ->findAll();
- }
-
- return $this;
- }
-
- /**
* Format projects to be displayed in the Gantt chart
*
* @access public
@@ -52,10 +20,11 @@ class ProjectGanttFormatter extends Project implements FormatterInterface
*/
public function format()
{
+ $projects = $this->query->findAll();
$colors = $this->color->getDefaultColors();
$bars = array();
- foreach ($this->projects as $project) {
+ foreach ($projects as $project) {
$start = empty($project['start_date']) ? time() : strtotime($project['start_date']);
$end = empty($project['end_date']) ? $start : strtotime($project['end_date']);
$color = next($colors) ?: reset($colors);
diff --git a/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php b/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php
new file mode 100644
index 00000000..c5d4e2be
--- /dev/null
+++ b/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Filter\FormatterInterface;
+
+class SubtaskTimeTrackingCalendarFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Format calendar events
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $events = array();
+
+ foreach ($this->query->findAll() as $row) {
+ $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
+
+ $events[] = array(
+ 'id' => $row['id'],
+ 'subtask_id' => $row['subtask_id'],
+ 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
+ 'start' => date('Y-m-d\TH:i:s', $row['start']),
+ 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
+ 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
+ 'borderColor' => $this->color->getBorderColor($row['color_id']),
+ 'textColor' => 'black',
+ 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
+ 'editable' => false,
+ );
+ }
+
+ return $events;
+ }
+}
diff --git a/app/Formatter/TaskFilterAutoCompleteFormatter.php b/app/Formatter/TaskAutoCompleteFormatter.php
index c9af4654..480ee797 100644
--- a/app/Formatter/TaskFilterAutoCompleteFormatter.php
+++ b/app/Formatter/TaskAutoCompleteFormatter.php
@@ -2,19 +2,19 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
use Kanboard\Model\Task;
-use Kanboard\Model\TaskFilter;
/**
- * Autocomplete formatter for task filter
+ * Task AutoComplete Formatter
*
- * @package formatter
- * @author Frederic Guillot
+ * @package formatter
+ * @author Frederic Guillot
*/
-class TaskFilterAutoCompleteFormatter extends TaskFilter implements FormatterInterface
+class TaskAutoCompleteFormatter extends BaseFormatter implements FormatterInterface
{
/**
- * Format the tasks for the ajax autocompletion
+ * Apply formatter
*
* @access public
* @return array
diff --git a/app/Formatter/TaskFilterCalendarFormatter.php b/app/Formatter/TaskCalendarFormatter.php
index 1b5d6ca4..60b9a062 100644
--- a/app/Formatter/TaskFilterCalendarFormatter.php
+++ b/app/Formatter/TaskCalendarFormatter.php
@@ -2,15 +2,37 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
+
/**
* Calendar event formatter for task filter
*
* @package formatter
* @author Frederic Guillot
*/
-class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface
+class TaskCalendarFormatter extends BaseTaskCalendarFormatter implements FormatterInterface
{
/**
+ * Full day event flag
+ *
+ * @access private
+ * @var boolean
+ */
+ private $fullDay = false;
+
+ /**
+ * When called calendar events will be full day
+ *
+ * @access public
+ * @return FormatterInterface
+ */
+ public function setFullDay()
+ {
+ $this->fullDay = true;
+ return $this;
+ }
+
+ /**
* Transform tasks to calendar events
*
* @access public
@@ -31,8 +53,8 @@ class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements For
'url' => $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]),
'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()),
- 'editable' => $this->isFullDay(),
- 'allday' => $this->isFullDay(),
+ 'editable' => $this->fullDay,
+ 'allday' => $this->fullDay,
);
}
@@ -47,6 +69,6 @@ class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements For
*/
private function getDateTimeFormat()
{
- return $this->isFullDay() ? 'Y-m-d' : 'Y-m-d\TH:i:s';
+ return $this->fullDay ? 'Y-m-d' : 'Y-m-d\TH:i:s';
}
}
diff --git a/app/Formatter/TaskFilterGanttFormatter.php b/app/Formatter/TaskGanttFormatter.php
index a4eef1ee..3209aa37 100644
--- a/app/Formatter/TaskFilterGanttFormatter.php
+++ b/app/Formatter/TaskGanttFormatter.php
@@ -2,15 +2,15 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * Gantt chart formatter for task filter
+ * Task Gantt Formatter
*
- * @package formatter
- * @author Frederic Guillot
+ * @package formatter
+ * @author Frederic Guillot
*/
-class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface
+class TaskGanttFormatter extends BaseFormatter implements FormatterInterface
{
/**
* Local cache for project columns
@@ -19,9 +19,9 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface
* @var array
*/
private $columns = array();
-
+
/**
- * Format tasks to be displayed in the Gantt chart
+ * Apply formatter
*
* @access public
* @return array
diff --git a/app/Formatter/TaskFilterICalendarFormatter.php b/app/Formatter/TaskICalFormatter.php
index 25b3aea0..a149f725 100644
--- a/app/Formatter/TaskFilterICalendarFormatter.php
+++ b/app/Formatter/TaskICalFormatter.php
@@ -6,14 +6,15 @@ use DateTime;
use Eluceo\iCal\Component\Calendar;
use Eluceo\iCal\Component\Event;
use Eluceo\iCal\Property\Event\Attendees;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * iCal event formatter for task filter
+ * iCal event formatter for tasks
*
* @package formatter
* @author Frederic Guillot
*/
-class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface
+class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterInterface
{
/**
* Calendar object
@@ -39,7 +40,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
*
* @access public
* @param \Eluceo\iCal\Component\Calendar $vCalendar
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function setCalendar(Calendar $vCalendar)
{
@@ -48,10 +49,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Transform results to ical events
+ * Transform results to iCal events
*
* @access public
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function addDateTimeEvents()
{
@@ -73,10 +74,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Transform results to all day ical events
+ * Transform results to all day iCal events
*
* @access public
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function addFullDayEvents()
{
@@ -96,7 +97,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Get common events for task ical events
+ * Get common events for task iCal events
*
* @access protected
* @param array $task
diff --git a/app/Formatter/UserFilterAutoCompleteFormatter.php b/app/Formatter/UserAutoCompleteFormatter.php
index b98e0d69..c46a24d0 100644
--- a/app/Formatter/UserFilterAutoCompleteFormatter.php
+++ b/app/Formatter/UserAutoCompleteFormatter.php
@@ -3,15 +3,15 @@
namespace Kanboard\Formatter;
use Kanboard\Model\User;
-use Kanboard\Model\UserFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * Autocomplete formatter for user filter
+ * Auto-complete formatter for user filter
*
* @package formatter
* @author Frederic Guillot
*/
-class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInterface
+class UserAutoCompleteFormatter extends BaseFormatter implements FormatterInterface
{
/**
* Format the tasks for the ajax autocompletion
diff --git a/app/Helper/AvatarHelper.php b/app/Helper/AvatarHelper.php
index c4e27ed9..a36d9b4a 100644
--- a/app/Helper/AvatarHelper.php
+++ b/app/Helper/AvatarHelper.php
@@ -20,16 +20,17 @@ class AvatarHelper extends Base
* @param string $username
* @param string $name
* @param string $email
+ * @param string $avatar_path
* @param string $css
* @param int $size
* @return string
*/
- public function render($user_id, $username, $name, $email, $css = 'avatar-left', $size = 48)
+ public function render($user_id, $username, $name, $email, $avatar_path, $css = 'avatar-left', $size = 48)
{
if (empty($user_id) && empty($username)) {
$html = $this->avatarManager->renderDefault($size);
} else {
- $html = $this->avatarManager->render($user_id, $username, $name, $email, $size);
+ $html = $this->avatarManager->render($user_id, $username, $name, $email, $avatar_path, $size);
}
return '<div class="avatar avatar-'.$size.' '.$css.'">'.$html.'</div>';
@@ -39,26 +40,29 @@ class AvatarHelper extends Base
* Render small user avatar
*
* @access public
- * @param string $user_id
- * @param string $username
- * @param string $name
- * @param string $email
+ * @param string $user_id
+ * @param string $username
+ * @param string $name
+ * @param string $email
+ * @param string $avatar_path
+ * @param string $css
* @return string
*/
- public function small($user_id, $username, $name, $email, $css = '')
+ public function small($user_id, $username, $name, $email, $avatar_path, $css = '')
{
- return $this->render($user_id, $username, $name, $email, $css, 20);
+ return $this->render($user_id, $username, $name, $email, $avatar_path, $css, 20);
}
/**
* Get a small avatar for the current user
*
* @access public
+ * @param string $css
* @return string
*/
public function currentUserSmall($css = '')
{
$user = $this->userSession->getAll();
- return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $css);
+ return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css);
}
}
diff --git a/app/Helper/CalendarHelper.php b/app/Helper/CalendarHelper.php
new file mode 100644
index 00000000..d5f4af21
--- /dev/null
+++ b/app/Helper/CalendarHelper.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskDueDateRangeFilter;
+use Kanboard\Formatter\SubtaskTimeTrackingCalendarFormatter;
+use Kanboard\Formatter\TaskCalendarFormatter;
+
+/**
+ * Calendar Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class CalendarHelper extends Base
+{
+ /**
+ * Get formatted calendar task due events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getTaskDateDueEvents(QueryBuilder $queryBuilder, $start, $end)
+ {
+ $formatter = new TaskCalendarFormatter($this->container);
+ $formatter->setFullDay();
+ $formatter->setColumns('date_due');
+
+ return $queryBuilder
+ ->withFilter(new TaskDueDateRangeFilter(array($start, $end)))
+ ->format($formatter);
+ }
+
+ /**
+ * Get formatted calendar task events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getTaskEvents(QueryBuilder $queryBuilder, $start, $end)
+ {
+ $startColumn = $this->config->get('calendar_project_tasks', 'date_started');
+
+ $queryBuilder->getQuery()->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ $startColumn,
+ 'date_due'
+ ));
+
+ $formatter = new TaskCalendarFormatter($this->container);
+ $formatter->setColumns($startColumn, 'date_due');
+
+ return $queryBuilder->format($formatter);
+ }
+
+ /**
+ * Get formatted calendar subtask time tracking events
+ *
+ * @access public
+ * @param integer $user_id
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getSubtaskTimeTrackingEvents($user_id, $start, $end)
+ {
+ $formatter = new SubtaskTimeTrackingCalendarFormatter($this->container);
+ return $formatter
+ ->withQuery($this->subtaskTimeTracking->getUserQuery($user_id)
+ ->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ 'start',
+ 'end'
+ ))
+ )
+ ->format();
+ }
+
+ /**
+ * Build SQL condition for a given time range
+ *
+ * @access public
+ * @param string $start_time Start timestamp
+ * @param string $end_time End timestamp
+ * @param string $start_column Start column name
+ * @param string $end_column End column name
+ * @return string
+ */
+ public function getCalendarCondition($start_time, $end_time, $start_column, $end_column)
+ {
+ $start_column = $this->db->escapeIdentifier($start_column);
+ $end_column = $this->db->escapeIdentifier($end_column);
+
+ $conditions = array(
+ "($start_column >= '$start_time' AND $start_column <= '$end_time')",
+ "($start_column <= '$start_time' AND $end_column >= '$start_time')",
+ "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))",
+ );
+
+ return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')';
+ }
+}
diff --git a/app/Helper/ICalHelper.php b/app/Helper/ICalHelper.php
new file mode 100644
index 00000000..dc399bf8
--- /dev/null
+++ b/app/Helper/ICalHelper.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskDueDateRangeFilter;
+use Kanboard\Formatter\TaskICalFormatter;
+use Eluceo\iCal\Component\Calendar as iCalendar;
+
+/**
+ * ICal Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class ICalHelper extends Base
+{
+ /**
+ * Get formatted calendar task due events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param iCalendar $calendar
+ * @param string $start
+ * @param string $end
+ */
+ public function addTaskDateDueEvents(QueryBuilder $queryBuilder, iCalendar $calendar, $start, $end)
+ {
+ $queryBuilder->withFilter(new TaskDueDateRangeFilter(array($start, $end)));
+
+ $formatter = new TaskICalFormatter($this->container);
+ $formatter->setColumns('date_due');
+ $formatter->setCalendar($calendar);
+ $formatter->withQuery($queryBuilder->getQuery());
+ $formatter->addFullDayEvents();
+ }
+}
diff --git a/app/Helper/ProjectActivityHelper.php b/app/Helper/ProjectActivityHelper.php
new file mode 100644
index 00000000..0638a978
--- /dev/null
+++ b/app/Helper/ProjectActivityHelper.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+use Kanboard\Filter\ProjectActivityProjectIdFilter;
+use Kanboard\Filter\ProjectActivityProjectIdsFilter;
+use Kanboard\Filter\ProjectActivityTaskIdFilter;
+use Kanboard\Formatter\ProjectActivityEventFormatter;
+use Kanboard\Model\ProjectActivity;
+
+/**
+ * Project Activity Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class ProjectActivityHelper extends Base
+{
+ /**
+ * Search events
+ *
+ * @access public
+ * @param string $search
+ * @return array
+ */
+ public function searchEvents($search)
+ {
+ $projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
+ $events = array();
+
+ if ($search !== '') {
+ $queryBuilder = $this->projectActivityLexer->build($search);
+ $queryBuilder
+ ->withFilter(new ProjectActivityProjectIdsFilter(array_keys($projects)))
+ ->getQuery()
+ ->desc(ProjectActivity::TABLE.'.id')
+ ->limit(500)
+ ;
+
+ $events = $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+
+ return $events;
+ }
+
+ /**
+ * Get project activity events
+ *
+ * @access public
+ * @param integer $project_id
+ * @param int $limit
+ * @return array
+ */
+ public function getProjectEvents($project_id, $limit = 50)
+ {
+ $queryBuilder = $this->projectActivityQuery
+ ->withFilter(new ProjectActivityProjectIdFilter($project_id));
+
+ $queryBuilder->getQuery()
+ ->desc(ProjectActivity::TABLE.'.id')
+ ->limit($limit)
+ ;
+
+ return $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+
+ /**
+ * Get projects activity events
+ *
+ * @access public
+ * @param int[] $project_ids
+ * @param int $limit
+ * @return array
+ */
+ public function getProjectsEvents(array $project_ids, $limit = 50)
+ {
+ $queryBuilder = $this->projectActivityQuery
+ ->withFilter(new ProjectActivityProjectIdsFilter($project_ids));
+
+ $queryBuilder->getQuery()
+ ->desc(ProjectActivity::TABLE.'.id')
+ ->limit($limit)
+ ;
+
+ return $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+
+ /**
+ * Get task activity events
+ *
+ * @access public
+ * @param integer $task_id
+ * @return array
+ */
+ public function getTaskEvents($task_id)
+ {
+ $queryBuilder = $this->projectActivityQuery
+ ->withFilter(new ProjectActivityTaskIdFilter($task_id));
+
+ $queryBuilder->getQuery()->desc(ProjectActivity::TABLE.'.id');
+
+ return $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
+ }
+}
diff --git a/app/Helper/ProjectHeaderHelper.php b/app/Helper/ProjectHeaderHelper.php
new file mode 100644
index 00000000..19570059
--- /dev/null
+++ b/app/Helper/ProjectHeaderHelper.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+
+/**
+ * Project Header Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class ProjectHeaderHelper extends Base
+{
+ /**
+ * Get current search query
+ *
+ * @access public
+ * @param array $project
+ * @return string
+ */
+ public function getSearchQuery(array $project)
+ {
+ $search = $this->request->getStringParam('search', $this->userSession->getFilters($project['id']));
+ $this->userSession->setFilters($project['id'], $search);
+ return urldecode($search);
+ }
+
+ /**
+ * Render project header (views switcher and search box)
+ *
+ * @access public
+ * @param array $project
+ * @param string $controller
+ * @param string $action
+ * @param bool $boardView
+ * @return string
+ */
+ public function render(array $project, $controller, $action, $boardView = false)
+ {
+ $filters = array(
+ 'controller' => $controller,
+ 'action' => $action,
+ 'project_id' => $project['id'],
+ 'search' => $this->getSearchQuery($project),
+ );
+
+ return $this->template->render('project_header/header', array(
+ 'project' => $project,
+ 'filters' => $filters,
+ 'categories_list' => $this->category->getList($project['id'], false),
+ 'users_list' => $this->projectUserRole->getAssignableUsersList($project['id'], false),
+ 'custom_filters_list' => $this->customFilter->getAll($project['id'], $this->userSession->getId()),
+ 'board_view' => $boardView,
+ ));
+ }
+
+ /**
+ * Get project description
+ *
+ * @access public
+ * @param array &$project
+ * @return string
+ */
+ public function getDescription(array &$project)
+ {
+ if ($project['owner_id'] > 0) {
+ $description = t('Project owner: ').'**'.$this->helper->text->e($project['owner_name'] ?: $project['owner_username']).'**'.PHP_EOL.PHP_EOL;
+
+ if (! empty($project['description'])) {
+ $description .= '***'.PHP_EOL.PHP_EOL;
+ $description .= $project['description'];
+ }
+ } else {
+ $description = $project['description'];
+ }
+
+ return $description;
+ }
+}
diff --git a/app/Helper/UserHelper.php b/app/Helper/UserHelper.php
index ee7d8ba5..c3369dfd 100644
--- a/app/Helper/UserHelper.php
+++ b/app/Helper/UserHelper.php
@@ -34,7 +34,7 @@ class UserHelper extends Base
{
$initials = '';
- foreach (explode(' ', $name) as $string) {
+ foreach (explode(' ', $name, 2) as $string) {
$initials .= mb_substr($string, 0, 1);
}
diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php
index c9abcd08..fadf0a1b 100644
--- a/app/Locale/bs_BA/translations.php
+++ b/app/Locale/bs_BA/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Svi projekti',
'Add a new column' => 'Dodaj novu kolonu',
'Title' => 'Naslov',
- 'Nobody assigned' => 'Niko nije dodijeljen',
'Assigned to %s' => 'Dodijeljen korisniku %s',
'Remove a column' => 'Ukloni kolonu',
'Remove a column from a board' => 'Ukloni kolonu sa table',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Broj zadataka',
'User' => 'Korisnik',
'Comments' => 'Komentari',
- 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown',
'Leave a comment' => 'Ostavi komentar',
'Comment is required' => 'Komentar je obavezan',
'Leave a description' => 'Dodaj opis',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Praćenje vremena:',
'New sub-task' => 'Novi pod-zadatak',
'New attachment added "%s"' => 'Ubačen novi prilog "%s"',
- 'Comment updated' => 'Komentar ažuriran',
'New comment posted by %s' => '%s ostavio novi komentar',
'New attachment' => 'Novi prilog',
'New comment' => 'Novi komentar',
+ 'Comment updated' => 'Komentar ažuriran',
'New subtask' => 'Novi pod-zadatak',
'Subtask updated' => 'Pod-zadatak ažuriran',
'Task updated' => 'Zadatak ažuriran',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvijek prihvatljiv, primjer: "%s", "%s"',
'New private project' => 'Novi privatni projekat',
'This project is private' => 'Ovaj projekat je privatan',
- 'Type here to create a new sub-task' => 'Piši ovdje za kreiranje novog pod-zadatka',
'Add' => 'Dodaj',
'Start date' => 'Datum početka',
'Time estimated' => 'Procijenjeno vrijeme',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Izvoz zbirnog pregleda po danima za "%s"',
'Exports' => 'Izvozi',
'This export contains the number of tasks per column grouped per day.' => 'Ovaj izvoz sadržava broj zadataka po koloni grupisanih po danima.',
- 'Nothing to preview...' => 'Ništa za pokazati...',
- 'Preview' => 'Pregled',
- 'Write' => 'Piši',
'Active swimlanes' => 'Aktivne swimline trake',
'Add a new swimlane' => 'Dodaj novu swimline traku',
'Change default swimlane' => 'Preimenuj podrazumijevanu swimline traku',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Trajanje zadatka u danima',
'Days in this column' => 'Dani u ovoj koloni',
'%dd' => '%dd',
- 'Add a link' => 'Dodaj vezu',
'Add a new link' => 'Dodaj novu vezu',
'Do you really want to remove this link: "%s"?' => 'Da li zaista želite ukloniti ovu vezu: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Da li zaista želite ukloniti ovu vezu sa zadatkom #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Tok mojih aktivnosti',
'My calendar' => 'Moj kalendar',
'Search tasks' => 'Pretraga zadataka',
- 'Back to the calendar' => 'Vrati na kalendar',
- 'Filters' => 'Filteri',
'Reset filters' => 'Vrati filtere na početno',
'My tasks due tomorrow' => 'Moji zadaci koje treba završiti sutra',
'Tasks due today' => 'Zadaci koje treba završiti danas',
@@ -772,14 +763,14 @@ return array(
'List' => 'Lista',
'Filter' => 'Filter',
'Advanced search' => 'Napredna pretraga',
- 'Example of query: ' => 'Primjer za upit',
- 'Search by project: ' => 'Pretraga po projektu',
- 'Search by column: ' => 'Pretraga po koloni',
- 'Search by assignee: ' => 'Pretraga po izvršiocu',
- 'Search by color: ' => 'Pretraga po boji',
- 'Search by category: ' => 'Pretraga po kategoriji',
- 'Search by description: ' => 'Pretraga po opisu',
- 'Search by due date: ' => 'Pretraga po datumu završetka',
+ 'Example of query: ' => 'Primjer za upit: ',
+ 'Search by project: ' => 'Pretraga po projektu: ',
+ 'Search by column: ' => 'Pretraga po koloni: ',
+ 'Search by assignee: ' => 'Pretraga po izvršiocu: ',
+ 'Search by color: ' => 'Pretraga po boji: ',
+ 'Search by category: ' => 'Pretraga po kategoriji: ',
+ 'Search by description: ' => 'Pretraga po opisu: ',
+ 'Search by due date: ' => 'Pretraga po datumu završetka: ',
'Lead and Cycle time for "%s"' => 'Vrijeme upravljanje i vremenski ciklus za "%s"',
'Average time spent into each column for "%s"' => 'Prosjek utrošenog vremena u svakoj koloni za "%s"',
'Average time spent into each column' => 'Prosjek utrošenog vrmena u svakoj koloni',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Datum završetka:',
'There is no start date or end date for this project.' => 'Nema početnog ili krajnjeg datuma za ovaj projekat.',
'Projects Gantt chart' => 'Gantogram projekata',
- 'Link type' => 'Tip veze',
'Change task color when using a specific task link' => 'Promijeni boju zadatka kada se koristi određena veza na zadatku',
'Task link creation or modification' => 'Veza na zadatku je napravljena ili izmijenjena',
'Milestone' => 'Prekretnica',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Podijeljeno',
'Owner' => 'Vlasnik',
'Unread notifications' => 'Nepročitana obavještenja',
- 'My filters' => 'Moji filteri',
'Notification methods:' => 'Metode obavještenja:',
'Import tasks from CSV file' => 'Uvezi zadatke putem CSV fajla',
'Unable to read your file' => 'Nemoguće pročitati fajl',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Korisničko ime mora biti malim slovima i jedinstveno',
'Passwords will be encrypted if present' => 'Šifra će biti kriptovana',
'%s attached a new file to the task %s' => '%s je dodano novi fajl u zadatak %s',
+ 'Link type' => 'Tip veze',
'Assign automatically a category based on a link' => 'Automatsko pridruživanje kategorije bazirano na vezi',
'BAM - Konvertible Mark' => 'BAM - Konvertibilna marka',
'Assignee Username' => 'Pridruži korisničko ime',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Zatvori zadatak kada nema aktivnosti',
'Duration in days' => 'Dužina trajanja u danima',
'Send email when there is no activity on a task' => 'Pošalji email kada nema aktivnosti na zadatku',
- 'List of external links' => 'Lista vanjskih veza',
'Unable to fetch link information.' => 'Ne mogu da pribavim informacije o vezi.',
'Daily background job for tasks' => 'Dnevni pozadinski poslovi na zadacima',
'Auto' => 'Automatski',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Vanjska veza',
'Copy and paste your link here...' => 'Kopiraj i zalijepi svoju vezu ovdje...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'Trenutno nema vanjskih veza.',
'Internal links' => 'Unutrašnje veze',
- 'There is no internal link for the moment.' => 'Trenutno nema unutrašnjih veza.',
'Assign to me' => 'Dodijeli meni',
'Me' => 'Za mene',
'Do not duplicate anything' => 'Ništa ne dupliciraj',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Menadžment korisnika',
'Groups management' => 'Menadžment grupa',
'Create from another project' => 'Napravi iz drugog projekta',
- 'There is no subtask at the moment.' => 'Trenutno nema pod-zadataka.',
'open' => 'otvoreno',
'closed' => 'zatvoreno',
'Priority:' => 'Prioritet:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Početo:',
'Moved:' => 'Pomjereno:',
'Task #%d' => 'Zadatak #%d',
- 'Sub-tasks' => 'Pod-zadaci',
'Date and time format' => 'Format za datum i vrijeme',
'Time format' => 'Format za vrijeme',
'Start date: ' => 'Početni datum:',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Korisnički filteri',
'Category filters' => 'Kategorija filtera',
'Upload a file' => 'Dodaj fajl',
- 'There is no attachment at the moment.' => 'Trenutno nema priloga.',
'View file' => 'Pregled fajla',
'Last activity' => 'Posljednja aktivnost',
'Change subtask position' => 'Promijeni poziciju pod-zadatka',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'Trenutno nema akcija.',
'Import actions from another project' => 'Uvezi akcije iz drugog projekta',
'There is no available project.' => 'Trenutno nema dostupnih projekata.',
+ 'Local File' => 'Lokalni fajl',
+ 'Configuration' => 'Konfiguracija',
+ 'PHP version:' => 'Verzija PHP-a:',
+ 'PHP SAPI:' => 'Verzija SAPI-a:',
+ 'OS version:' => 'Verzija OS-a:',
+ 'Database version:' => 'Verzija baze podataka:',
+ 'Browser:' => 'Pretraživač:',
+ 'Task view' => 'Pregled zadatka',
+ 'Edit task' => 'Uredi zadatak',
+ 'Edit description' => 'Uredi opis',
+ 'New internal link' => 'Nova unutrašnja veza',
+ 'Display list of keyboard shortcuts' => 'Prikaži listu prečica na tastaturi',
+ 'Menu' => 'Meni',
+ 'Set start date' => 'Postavi početni datum',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Dodaj sliku za moj avatar',
+ 'Remove my image' => 'Ukloni moju sliku',
+ 'The OAuth2 state parameter is invalid' => 'OAuth2 status parametar nije validan',
+ 'User not found.' => 'Korisnik nije pronađen.',
+ 'Search in activity stream' => 'Pretraži aktivnosti',
+ 'My activities' => 'Moje aktivnosti',
+ 'Activity until yesterday' => 'Aktivnosti do jučer',
+ 'Activity until today' => 'Aktivnosti do danas',
+ 'Search by creator: ' => 'Pretraga po kreatoru: ',
+ 'Search by creation date: ' => 'Pretraga po datumu kreiranja: ',
+ 'Search by task status: ' => 'Pretraga po statusu zadatka: ',
+ 'Search by task title: ' => 'Pretraga po naslovu zadatka: ',
+ 'Activity stream search' => 'Pretraga aktivnosti',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php
index 08836dec..777e9b42 100644
--- a/app/Locale/cs_CZ/translations.php
+++ b/app/Locale/cs_CZ/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Všechny projekty',
'Add a new column' => 'Přidat nový sloupec',
'Title' => 'Název',
- 'Nobody assigned' => 'Nepřiřazena žádná osoba',
'Assigned to %s' => 'Přiřazeno uživateli: %s',
'Remove a column' => 'Vyjmout sloupec',
'Remove a column from a board' => 'Vyjmout sloupec z nástěnky',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Počet úkolů',
'User' => 'Uživatel',
'Comments' => 'Komentáře',
- 'Write your text in Markdown' => 'Můžete použít i Markdown-syntaxi',
'Leave a comment' => 'Zanechte komentář',
'Comment is required' => 'Komentář je vyžadován',
'Leave a description' => 'Vložte popis',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Sledování času:',
'New sub-task' => 'Nový dílčí úkol',
'New attachment added "%s"' => 'Byla přidána nová příloha "%s".',
- 'Comment updated' => 'Komentář byl aktualizován.',
'New comment posted by %s' => 'Nový komentář publikovaný uživatelem %s',
'New attachment' => 'Nová příloha',
'New comment' => 'Nový komentář',
+ 'Comment updated' => 'Komentář byl aktualizován.',
'New subtask' => 'Nový dílčí úkol',
'Subtask updated' => 'Dílčí úkol byl aktualizován',
'Task updated' => 'Úkol byl aktualizován',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formát je vždy akceptován, například: "%s" a "%s"',
'New private project' => 'Nový soukromý projekt',
'This project is private' => 'Tento projekt je soukromuý',
- 'Type here to create a new sub-task' => 'Uveďte zde pro vytvoření nového dílčího úkolu',
'Add' => 'Přidat',
'Start date' => 'Počáteční datum',
'Time estimated' => 'Odhadovaný čas',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export denních přehledů pro "%s"',
'Exports' => 'Exporty',
'This export contains the number of tasks per column grouped per day.' => 'Tento export obsahuje počet úkolů pro jednotlivé sloupce seskupených podle dní.',
- 'Nothing to preview...' => 'Žádná položka k zobrazení ...',
- 'Preview' => 'Náhled',
- 'Write' => 'Režim psaní',
'Active swimlanes' => 'Aktive Swimlane',
'Add a new swimlane' => 'Přidat nový řádek',
'Change default swimlane' => 'Standard Swimlane ändern',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Doba trvání úkolu ve dnech',
'Days in this column' => 'Dní v tomto sloupci',
'%dd' => '%d d',
- 'Add a link' => 'Přidat odkaz',
'Add a new link' => 'Přidat nový odkaz',
'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?',
'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Přehled mých aktivit',
'My calendar' => 'Můj kalendář',
'Search tasks' => 'Hledání úkolů',
- 'Back to the calendar' => 'Zpět do kalendáře',
- 'Filters' => 'Filtry',
'Reset filters' => 'Resetovat filtry',
'My tasks due tomorrow' => 'Moje zítřejší úkoly',
'Tasks due today' => 'Dnešní úkoly',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 8e686ee4..7c255561 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alle Projekter',
'Add a new column' => 'Tilføj en ny kolonne',
'Title' => 'Titel',
- 'Nobody assigned' => 'Ingen ansvarlig',
'Assigned to %s' => 'Ansvarlig: %s',
'Remove a column' => 'Fjern en kolonne',
'Remove a column from a board' => 'Fjern en kolonne fra et board',
@@ -166,7 +165,6 @@ return array(
// 'Task count' => '',
'User' => 'Bruger',
'Comments' => 'Kommentarer',
- 'Write your text in Markdown' => 'Skriv din tekst i markdown',
'Leave a comment' => 'Efterlad en kommentar',
'Comment is required' => 'Kommentar er krævet',
'Leave a description' => 'Efterlad en beskrivelse...',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Tidsmåling:',
'New sub-task' => 'Ny under-opgave',
'New attachment added "%s"' => 'Ny vedhæftning tilføjet "%s"',
- 'Comment updated' => 'Kommentar opdateret',
'New comment posted by %s' => 'Ny kommentar af %s',
// 'New attachment' => '',
// 'New comment' => '',
+ 'Comment updated' => 'Kommentar opdateret',
// 'New subtask' => '',
// 'Subtask updated' => '',
// 'Task updated' => '',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er altid accepteret, eksempelvis: "%s" og "%s"',
'New private project' => 'Nyt privat projekt',
'This project is private' => 'Dette projekt er privat',
- 'Type here to create a new sub-task' => 'Skriv her for at tilføje en ny under-opgave',
'Add' => 'Tilføj',
'Start date' => 'Start dato',
'Time estimated' => 'Tid estimeret',
@@ -483,9 +480,6 @@ return array(
// 'Daily project summary export for "%s"' => '',
// 'Exports' => '',
// 'This export contains the number of tasks per column grouped per day.' => '',
- // 'Nothing to preview...' => '',
- // 'Preview' => '',
- // 'Write' => '',
// 'Active swimlanes' => '',
// 'Add a new swimlane' => '',
// 'Change default swimlane' => '',
@@ -539,7 +533,6 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
- // 'Add a link' => '',
// 'Add a new link' => '',
// 'Do you really want to remove this link: "%s"?' => '',
// 'Do you really want to remove this link with task #%d?' => '',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 59c16e11..43b80561 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alle Projekte',
'Add a new column' => 'Neue Spalte hinzufügen',
'Title' => 'Titel',
- 'Nobody assigned' => 'Nicht zugeordnet',
'Assigned to %s' => 'Zuständig: %s',
'Remove a column' => 'Spalte löschen',
'Remove a column from a board' => 'Spalte einer Pinnwand löschen',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Aufgabenanzahl',
'User' => 'Benutzer',
'Comments' => 'Kommentare',
- 'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax',
'Leave a comment' => 'Kommentar eingeben',
'Comment is required' => 'Ein Kommentar wird benötigt',
'Leave a description' => 'Beschreibung eingeben',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Zeittracking',
'New sub-task' => 'Neue Teilaufgabe',
'New attachment added "%s"' => 'Neuer Anhang "%s" wurde hinzugefügt.',
- 'Comment updated' => 'Kommentar wurde aktualisiert',
'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s',
'New attachment' => 'Neuer Anhang',
'New comment' => 'Neuer Kommentar',
+ 'Comment updated' => 'Kommentar wurde aktualisiert',
'New subtask' => 'Neue Teilaufgabe',
'Subtask updated' => 'Teilaufgabe aktualisiert',
'Task updated' => 'Aufgabe aktualisiert',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO Format wird immer akzeptiert, z.B.: "%s" und "%s"',
'New private project' => 'Neues privates Projekt',
'This project is private' => 'Dieses Projekt ist privat',
- 'Type here to create a new sub-task' => 'Hier tippen, um eine neue Teilaufgabe zu erstellen',
'Add' => 'Hinzufügen',
'Start date' => 'Startdatum',
'Time estimated' => 'Geschätzte Zeit',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export der täglichen Projektzusammenfassung für "%s"',
'Exports' => 'Exporte',
'This export contains the number of tasks per column grouped per day.' => 'Dieser Export enthält die Anzahl der Aufgaben pro Spalte nach Tagen gruppiert.',
- 'Nothing to preview...' => 'Nichts in der Vorschau anzuzeigen ...',
- 'Preview' => 'Vorschau',
- 'Write' => 'Ändern',
'Active swimlanes' => 'Aktive Swimlane',
'Add a new swimlane' => 'Eine neue Swimlane hinzufügen',
'Change default swimlane' => 'Standard-Swimlane ändern',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Aufgabenalter in Tagen',
'Days in this column' => 'Tage in dieser Spalte',
'%dd' => '%dT',
- 'Add a link' => 'Verbindung hinzufügen',
'Add a new link' => 'Neue Verbindung hinzufügen',
'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?',
'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?',
@@ -627,9 +620,9 @@ return array(
'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.',
'Code' => 'Code',
'Two factor authentication' => 'Zwei-Faktor-Authentifizierung',
- 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI',
+ 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI: ',
'Check my code' => 'Überprüfe meinen Code',
- 'Secret key: ' => 'Geheimer Schlüssel',
+ 'Secret key: ' => 'Geheimer Schlüssel: ',
'Test your device' => 'Teste dein Gerät',
'Assign a color when the task is moved to a specific column' => 'Weise eine Farbe zu, wenn die Aufgabe zu einer bestimmten Spalte bewegt wird',
'%s via Kanboard' => '%s via Kanboard',
@@ -660,15 +653,15 @@ return array(
'Timeframe to calculate new due date' => 'Zeitfenster zur Berechnung für neues Ablaufdatum',
'Base date to calculate new due date' => 'Basisdatum zur Berechnung für neues Ablaufdatum',
'Action date' => 'Aktionsdatum',
- 'Base date to calculate new due date: ' => 'Basisdatum zur Berechnung für neues Ablaufdatum:',
- 'This task has created this child task: ' => 'Diese Aufgabe hat diese Teilaufgabe erstellt:',
+ 'Base date to calculate new due date: ' => 'Basisdatum zur Berechnung für neues Ablaufdatum: ',
+ 'This task has created this child task: ' => 'Diese Aufgabe hat diese Teilaufgabe erstellt: ',
'Day(s)' => 'Tag(e)',
'Existing due date' => 'Existierendes Ablaufdatum',
- 'Factor to calculate new due date: ' => 'Faktor zur Berechnung für neues Ablaufdatum',
+ 'Factor to calculate new due date: ' => 'Faktor zur Berechnung für neues Ablaufdatum: ',
'Month(s)' => 'Monat(e)',
'Recurrence' => 'Wiederholung',
- 'This task has been created by: ' => 'DIese Aufgabe wurde erstellt von:',
- 'Recurrent task has been generated:' => 'Wiederkehrende Aufgabe wurde erstellt',
+ 'This task has been created by: ' => 'DIese Aufgabe wurde erstellt von: ',
+ 'Recurrent task has been generated:' => 'Wiederkehrende Aufgabe wurde erstellt ',
'Timeframe to calculate new due date: ' => 'Zeitfenster zur Berechnung für neues Ablaufdatum: ',
'Trigger to generate recurrent task: ' => 'Auslöser für wiederkehrende Aufgabe: ',
'When task is closed' => 'Wenn Aufgabe geshlossen wird',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Aktivitätsstream',
'My calendar' => 'Mein Kalender',
'Search tasks' => 'Suche nach Aufgaben',
- 'Back to the calendar' => 'Zurück zum Kalender',
- 'Filters' => 'Filter',
'Reset filters' => 'Filter zurücksetzen',
'My tasks due tomorrow' => 'Meine morgen fälligen Aufgaben',
'Tasks due today' => 'Heute fällige Aufgaben',
@@ -786,15 +777,15 @@ return array(
'Average time spent' => 'Durchschnittlicher Zeitverbrauch',
'This chart show the average time spent into each column for the last %d tasks.' => 'Dieses Diagramm zeigt die durchschnittliche Zeit in jeder Spalte der letzten %d Aufgaben.',
'Average Lead and Cycle time' => 'Durchschnittliche Zyklus- und Durchlaufzeit',
- 'Average lead time: ' => 'Durchschnittliche Durchlaufzeit:',
- 'Average cycle time: ' => 'Durchschnittliche Zykluszeit:',
+ 'Average lead time: ' => 'Durchschnittliche Durchlaufzeit: ',
+ 'Average cycle time: ' => 'Durchschnittliche Zykluszeit: ',
'Cycle Time' => 'Zykluszeit',
'Lead Time' => 'Durchlaufzeit',
'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Das Diagramm zeigt die durchschnittliche Durchlauf- und Zykluszeit der letzten %d Aufgaben über die Zeit an.',
'Average time into each column' => 'Durchschnittzeit in jeder Spalte',
'Lead and cycle time' => 'Durchlauf- und Zykluszeit',
- 'Lead time: ' => 'Durchlaufzeit:',
- 'Cycle time: ' => 'Zykluszeit:',
+ 'Lead time: ' => 'Durchlaufzeit: ',
+ 'Cycle time: ' => 'Zykluszeit: ',
'Time spent into each column' => 'zeit verbracht in jeder Spalte',
'The lead time is the duration between the task creation and the completion.' => 'Die Durchlaufzeit ist die Dauer zwischen Erstellung und Fertigstellung.',
'The cycle time is the duration between the start date and the completion.' => 'Die Zykluszeit ist die Dauer zwischen Start und Fertigstellung.',
@@ -802,7 +793,7 @@ return array(
'Set automatically the start date' => 'Setze Startdatum automatisch',
'Edit Authentication' => 'Authentifizierung bearbeiten',
'Remote user' => 'Remote-Benutzer',
- 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel LDAP, Google und Github Accounts',
+ 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Remote-Benutzer haben kein Passwort in der Kanboard Datenbank, Beispiel: LDAP, Google und Github Accounts',
'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Wenn die Box "Verbiete Login-Formular" angeschaltet ist, werden Eingaben in das Login Formular ignoriert.',
'New remote user' => 'Neuer Remote-Benutzer',
'New local user' => 'Neuer lokaler Benutzer',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Endedatum:',
'There is no start date or end date for this project.' => 'Es gibt kein Startdatum oder Endedatum für dieses Projekt',
'Projects Gantt chart' => 'Projekt Gantt Diagramm',
- 'Link type' => 'Verbindungstyp',
'Change task color when using a specific task link' => 'Aufgabefarbe ändern bei bestimmter Aufgabenverbindung',
'Task link creation or modification' => 'Aufgabenverbindung erstellen oder bearbeiten',
'Milestone' => 'Meilenstein',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Geteilt',
'Owner' => 'Eigentümer',
'Unread notifications' => 'Ungelesene Benachrichtigungen',
- 'My filters' => 'Meine Filter',
'Notification methods:' => 'Benachrichtigungs-Methoden:',
'Import tasks from CSV file' => 'Importiere Aufgaben aus CSV Datei',
'Unable to read your file' => 'Die Datei kann nicht gelesen werden',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Benutzernamen müssen in Kleinbuschstaben und eindeutig sein',
'Passwords will be encrypted if present' => 'Passwörter werden verschlüsselt wenn vorhanden',
'%s attached a new file to the task %s' => '%s hat eine neue Datei zur Aufgabe %s hinzufgefügt',
+ 'Link type' => 'Verbindungstyp',
'Assign automatically a category based on a link' => 'Linkbasiert eine Kategorie automatisch zuordnen',
'BAM - Konvertible Mark' => 'BAM - Konvertible Mark',
'Assignee Username' => 'Benutzername des Zuständigen',
@@ -1032,7 +1022,7 @@ return array(
'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => 'Kein Plugin hat eine Projekt-Benachrichtigungsmethode registriert. Sie können individuelle Meldungen in Ihrem Benutzerprofil konfigurieren',
'My dashboard' => 'Mein Dashboard',
'My profile' => 'Mein Profil',
- 'Project owner: ' => 'Projekt-Besitzer:',
+ 'Project owner: ' => 'Projekt-Besitzer: ',
'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => 'Die Projekt-Kennung ist optional und muss alphanumerisch sein, beispielsweise: MYPROJECT.',
'Project owner' => 'Projekt-Besitzer',
'Those dates are useful for the project Gantt chart.' => 'Diese Daten sind nützlich für das Gantt-Diagramm.',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Schliesse eine Aufgabe, wenn keine Aktivitäten vorhanden sind',
'Duration in days' => 'Dauer in Tagen',
'Send email when there is no activity on a task' => 'Versende eine Email, wenn keine Aktivitäten an einer Aufgabe vorhanden sind',
- 'List of external links' => 'Liste der externen Verbindungen',
'Unable to fetch link information.' => 'Kann keine Informationen über Verbindungen holen',
'Daily background job for tasks' => 'Tägliche Hintergrundarbeit für Aufgaben',
'Auto' => 'Auto',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Externe Verbindung',
'Copy and paste your link here...' => 'Kopieren Sie Ihren Link hier...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'Es gibt im Moment keine externe Verbindung.',
'Internal links' => 'Interne Verbindungen',
- 'There is no internal link for the moment.' => 'Es gibt im Moment keine interne Verbindung.',
'Assign to me' => 'Mir zuweisen',
'Me' => 'Mich',
'Do not duplicate anything' => 'Nichts duplizieren',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Benutzermanagement',
'Groups management' => 'Gruppenmanagement',
'Create from another project' => 'Von einem anderen Projekt erstellen',
- 'There is no subtask at the moment.' => 'Es gibt im Moment keine Teilaufgabe',
'open' => 'offen',
'closed' => 'geschlossen',
'Priority:' => 'Priorität:',
@@ -1096,13 +1082,12 @@ return array(
'Started:' => 'Gestarted:',
'Moved:' => 'Verschoben:',
'Task #%d' => 'Aufgabe #%d',
- 'Sub-tasks' => 'Teilaufgaben',
'Date and time format' => 'Datums- und Zeitformat',
'Time format' => 'Zeitformat',
- 'Start date: ' => 'Anfangsdatum:',
- 'End date: ' => 'Enddatum:',
- 'New due date: ' => 'Neues Fälligkeitsdatum',
- 'Start date changed: ' => 'Anfangsdatum geändert:',
+ 'Start date: ' => 'Anfangsdatum: ',
+ 'End date: ' => 'Enddatum: ',
+ 'New due date: ' => 'Neues Fälligkeitsdatum: ',
+ 'Start date changed: ' => 'Anfangsdatum geändert: ',
'Disable private projects' => 'Private Projekte deaktivieren',
'Do you really want to remove this custom filter: "%s"?' => 'Wollen Sie diesen benutzerdefinierten Filter wirklich entfernen: "%s"?',
'Remove a custom filter' => 'Benutzerdefinierten Filter entfernen',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Benutzer-Filter',
'Category filters' => 'Kategorie-Filter',
'Upload a file' => 'Eine Datei hochladen',
- 'There is no attachment at the moment.' => 'Es gibt zur Zeit keine Anhänge',
'View file' => 'Datei ansehen',
'Last activity' => 'Letzte Aktivität',
'Change subtask position' => 'Position der Unteraufgabe ändern',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'Es gibt zur Zeit keine Aktionen.',
'Import actions from another project' => 'Aktionen von einem anderen Projekt importieren',
'There is no available project.' => 'Es ist kein Projekt verfügbar.',
+ 'Local File' => 'Lokale Datei',
+ 'Configuration' => 'Konfiguration',
+ 'PHP version:' => 'PHP Version:',
+ 'PHP SAPI:' => 'PHP SAPI:',
+ 'OS version:' => 'OS Version:',
+ 'Database version:' => 'Datenbank Version:',
+ 'Browser:' => 'Browser:',
+ 'Task view' => 'Aufgaben Ansicht',
+ 'Edit task' => 'Aufgabe bearbeiten',
+ 'Edit description' => 'Beschreibung bearbeiten',
+ 'New internal link' => 'Neue interne Verbindung',
+ 'Display list of keyboard shortcuts' => 'Liste der Tastaturkürzel anzeigen',
+ 'Menu' => 'Menü',
+ 'Set start date' => 'Anfangsdatum setzen',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Mein Avatar Bild hochladen',
+ 'Remove my image' => 'Mein Bild entfernen',
+ 'The OAuth2 state parameter is invalid' => 'Der OAuth2 Statusparameter ist ungültig',
+ 'User not found.' => 'Benutzer nicht gefunden',
+ 'Search in activity stream' => 'Im Aktivitätenstrom suchen',
+ 'My activities' => 'Meine Aktivitäten',
+ 'Activity until yesterday' => 'Aktivitäten bis gestern',
+ 'Activity until today' => 'Aktivitäten bis heute',
+ 'Search by creator: ' => 'nach Ersteller suchen',
+ 'Search by creation date: ' => 'nach Datum suchen',
+ 'Search by task status: ' => 'nach Aufgabenstatus suchen',
+ 'Search by task title: ' => 'nach Titel suchen',
+ 'Activity stream search' => 'Im Aktivitätenstrom suchen',
+ 'Projects where "%s" is manager' => 'Projekte in denen "%s" Manager ist',
+ 'Projects where "%s" is member' => 'Projekte in denen "%s" Mitglied ist',
+ 'Open tasks assigned to "%s"' => 'Offene Aufgaben, die "%s" zugeteilt sind',
+ 'Closed tasks assigned to "%s"' => 'Geschlossene Aufgaben, die "%s" zugeteilt sind',
);
diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php
index 1f58fb69..664bf328 100644
--- a/app/Locale/el_GR/translations.php
+++ b/app/Locale/el_GR/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Όλα τα έργα',
'Add a new column' => 'Πρόσθήκη στήλης',
'Title' => 'Τίτλος',
- 'Nobody assigned' => 'Δεν έχει ανατεθεί',
'Assigned to %s' => 'Ανατιθεμένο στον %s',
'Remove a column' => 'Αφαίρεση στήλης',
'Remove a column from a board' => 'Αφαίρεση στήλης από τον πίνακα',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Αρίθμηση εργασιών',
'User' => 'Χρήστης',
'Comments' => 'Σχόλια',
- 'Write your text in Markdown' => 'Δυνατότητα γραφής και σε Markdown',
'Leave a comment' => 'Αφήστε ένα σχόλιο',
'Comment is required' => 'Το σχόλιο απαιτείται',
'Leave a description' => 'Αφήστε μια περιγραφή',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Παρακολούθηση του χρόνου:',
'New sub-task' => 'Νέα υπο-εργασία',
'New attachment added "%s"' => 'Νέα επικόλληση προστέθηκε « %s »',
- 'Comment updated' => 'Το σχόλιο ενημερώθηκε',
'New comment posted by %s' => 'Νέο σχόλιο από τον χρήστη « %s »',
'New attachment' => 'New attachment',
'New comment' => 'Νέο σχόλιο',
+ 'Comment updated' => 'Το σχόλιο ενημερώθηκε',
'New subtask' => 'Νέα υπο-εργασία',
'Subtask updated' => 'Η Υπο-Εργασία ενημερώθηκε',
'Task updated' => 'Η εργασία ενημερώθηκε',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format είναι πάντα αποδεκτό, π.χ.: « %s » και « %s »',
'New private project' => 'Νέο ιδιωτικό έργο',
'This project is private' => 'Αυτό το έργο είναι ιδιωτικό',
- 'Type here to create a new sub-task' => 'Πληκτρολογήστε εδώ για να δημιουργήσετε μια νέα υπο-εργασία',
'Add' => 'Προσθήκη',
'Start date' => 'Ημερομηνία έναρξης',
'Time estimated' => 'Εκτιμώμενος χρόνος',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Εξαγωγή της καθημερινής περίληψης του έργου « %s »',
'Exports' => 'Εξαγωγές',
'This export contains the number of tasks per column grouped per day.' => 'Αυτή η κατάσταση περιέχει τον αριθμό των εργασιών ανά στήλη ομαδοποιημένα ανά ημέρα.',
- 'Nothing to preview...' => 'Τίποτα για προεπισκόπηση...',
- 'Preview' => 'Προεπισκόπηση',
- 'Write' => 'Write',
'Active swimlanes' => 'Ενεργές λωρίδες',
'Add a new swimlane' => 'Προσθήκη λωρίδας',
'Change default swimlane' => 'Αλλαγή της εξ\' ορισμού λωρίδας',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Χρόνος εργασίας σε μέρες',
'Days in this column' => 'Μέρες σε αυτή την στήλη',
'%dd' => '%dημ',
- 'Add a link' => 'Προσθήκη ενός link',
'Add a new link' => 'Προσθήκη ενός νέου link',
'Do you really want to remove this link: "%s"?' => 'Θέλετε σίγουρα να αφαιρέσετε αυτό το link : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Θέλετε σίγουρα να αφαιρέσετε αυτό το link του έργου n°%d ?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Η ροή δραστηριοτήτων μου',
'My calendar' => 'Το ημερολόγιο μου',
'Search tasks' => 'Αναζήτηση εργασιών',
- 'Back to the calendar' => 'Πίσω στο ημερολόγιο',
- 'Filters' => 'Φίλτρα',
'Reset filters' => 'Επαναφορά φίλτρων',
'My tasks due tomorrow' => 'Οι εργασίες καθηκόντων μου αύριο',
'Tasks due today' => 'Οι εργασίες καθηκόντων μου αύριο',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Ημερομηνία λήξης :',
'There is no start date or end date for this project.' => 'Δεν υπάρχει ημερομηνία έναρξης ή λήξης για το έργο αυτό.',
'Projects Gantt chart' => 'Διάγραμμα Gantt έργων',
- 'Link type' => 'Τύπος συνδέσμου',
'Change task color when using a specific task link' => 'Αλλαγή χρώματος εργασίας χρησιμοποιώντας συγκεκριμένο σύνδεσμο εργασίας',
'Task link creation or modification' => 'Σύνδεσμος δημιουργίας ή τροποποίησης εργασίας',
'Milestone' => 'Ορόσημο',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Διαμοιρασμένα',
'Owner' => 'Ιδιοκτήτης',
'Unread notifications' => 'Αδιάβαστες ειδοποιήσεις',
- 'My filters' => 'Τα φίλτρα μου',
'Notification methods:' => 'Μέθοδοι ειδοποίησης:',
'Import tasks from CSV file' => 'Εισαγωγή εργασιών μέσω αρχείου CSV',
'Unable to read your file' => 'Δεν είναι δυνατή η ανάγνωση του αρχείου',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Οι ονομασίες χρηστών πρέπει να είναι σε μικρά γράμματα (lowercase) και μοναδικά',
'Passwords will be encrypted if present' => 'Οι κωδικοί πρόσβασης κρυπτογραφούνται, αν υπάρχουν',
'%s attached a new file to the task %s' => '%s νέο συνημμένο αρχείο της εργασίας %s',
+ 'Link type' => 'Τύπος συνδέσμου',
'Assign automatically a category based on a link' => 'Ανατίθεται αυτόματα κατηγορία, βασισμένη στον σύνδεσμο',
'BAM - Konvertible Mark' => 'BAM - Konvertible Mark',
'Assignee Username' => 'Δικαιοδόχο όνομα χρήστη',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Κλείσιμο εργασίας όταν δεν υπάρχει δραστηριότητα',
'Duration in days' => 'Διάρκεια σε ημέρες',
'Send email when there is no activity on a task' => 'Αποστολή email όταν δεν υπάρχει δραστηριότητα σε εργασία',
- 'List of external links' => 'Λίστα εξωτερικών συνδέσμων',
'Unable to fetch link information.' => 'Δεν είναι δυνατή η ανάλυση της πληροφορίας συνδεσμου',
'Daily background job for tasks' => 'Ημερήσια παρασκηνιακή δουλειά για τις εργασίες',
'Auto' => 'Αυτόματο',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Εξωτερικός σύνδεσμος',
'Copy and paste your link here...' => 'Κάντε αντιγραφή και επικόλληση εδώ',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'Προς το παρών, δεν υπάρχουν εξωτερικοί σύνδεσμοι.',
'Internal links' => 'Εσωτερικοί σύνδεσμοι',
- 'There is no internal link for the moment.' => 'Προς το παρών, δεν υπάρχουν εσωτερικοί σύνδεσμοι.',
'Assign to me' => 'Αναττίθεται σε εμένα',
'Me' => 'Σε μένα',
'Do not duplicate anything' => 'Να μην γίνει κλωνοποίηση από άλλο έργο',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Διαχείριση χρηστών',
'Groups management' => 'Διαχείριση ομάδων',
'Create from another project' => 'Δημιουργία από άλλο έργο',
- 'There is no subtask at the moment.' => 'Προς το παρών, δεν υπάρχει καμία υπο-εργασία.',
'open' => 'Ανοικτό',
'closed' => 'Κλειστό',
'Priority:' => 'Προτεραιότητα:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Ξεκίνησε:',
'Moved:' => 'Μετακινήθηκε:',
'Task #%d' => 'Εργασία #%d',
- 'Sub-tasks' => 'Υπο-εργασίες',
'Date and time format' => 'Μορφή ημερομηνίας και ώρας',
'Time format' => 'Μορφή ώρας',
'Start date: ' => 'Ημερομηνία έναρξης: ',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Φίλτρα οριζόμενα από τον χρήστη',
'Category filters' => 'Κατηγορία φίλτρων',
'Upload a file' => 'Ανέβασμα αρχείου',
- 'There is no attachment at the moment.' => 'Προς το παρόν, δεν υπάρχουν συνημμένα',
'View file' => 'Προβολή αρχείου',
'Last activity' => 'Τελευταία δραστηριότητα',
'Change subtask position' => 'Αλλαγή θέσης υπο-εργασίας',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index bda20e61..6b4dda42 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Todos los proyectos',
'Add a new column' => 'Añadir una nueva columna',
'Title' => 'Título',
- 'Nobody assigned' => 'Nadie asignado',
'Assigned to %s' => 'Asignada a %s',
'Remove a column' => 'Suprimir esta columna',
'Remove a column from a board' => 'Suprimir una columna de un tablero',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Contador de tareas',
'User' => 'Usuario',
'Comments' => 'Comentarios',
- 'Write your text in Markdown' => 'Redacta el texto en Markdown',
'Leave a comment' => 'Dejar un comentario',
'Comment is required' => 'El comentario es obligatorio',
'Leave a description' => 'Dejar una descripción',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Control de tiempo:',
'New sub-task' => 'Nueva subtarea',
'New attachment added "%s"' => 'Nuevo adjunto agregado "%s"',
- 'Comment updated' => 'Comentario actualizado',
'New comment posted by %s' => 'Nuevo comentario agregado por %s',
'New attachment' => 'Nuevo adjunto',
'New comment' => 'Nuevo comentario',
+ 'Comment updated' => 'Comentario actualizado',
'New subtask' => 'Nueva subtarea',
'Subtask updated' => 'Subtarea actualizada',
'Task updated' => 'Tarea actualizada',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'El formato ISO siempre es aceptado, ejemplo: "%s" y "%s"',
'New private project' => 'Nuevo proyecto privado',
'This project is private' => 'Este proyecto es privado',
- 'Type here to create a new sub-task' => 'Escriba aquí para crear una nueva sub-tarea',
'Add' => 'Añadir',
'Start date' => 'Fecha de inicio',
'Time estimated' => 'Tiempo estimado',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Exportar sumario diario del proyecto para "%s"',
'Exports' => 'Exportaciones',
'This export contains the number of tasks per column grouped per day.' => 'Esta exportación contiene el número de tereas por columna agrupada por día.',
- 'Nothing to preview...' => 'Nada que previsualizar...',
- 'Preview' => 'Previsualizar',
- 'Write' => 'Grabar',
'Active swimlanes' => 'Calles activas',
'Add a new swimlane' => 'Añadir nueva calle',
'Change default swimlane' => 'Cambiar la calle por defecto',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Edad de la tarea en días',
'Days in this column' => 'Días en esta columna',
'%dd' => '%dd',
- 'Add a link' => 'Añadir enlace',
'Add a new link' => 'Añadir nuevo enlace',
'Do you really want to remove this link: "%s"?' => '¿Realmente quiere quitar este enlace: "%s"?',
'Do you really want to remove this link with task #%d?' => '¿Realmente quiere quitar este enlace con esta tarea: #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Mi flujo de actividad',
'My calendar' => 'Mi calendario',
'Search tasks' => 'Buscar tareas',
- 'Back to the calendar' => 'Volver al calendario',
- 'Filters' => 'Filtros',
'Reset filters' => 'Limpiar filtros',
'My tasks due tomorrow' => 'Mis tareas a entregar mañana',
'Tasks due today' => 'Tareas a antregar hoy',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Fecha final',
'There is no start date or end date for this project.' => 'No existe fecha de inicio o de fin para este proyecto.',
'Projects Gantt chart' => 'Diagramas de Gantt de los proyectos',
- 'Link type' => 'Tipo de enlace',
'Change task color when using a specific task link' => 'Cambiar colo de la tarea al usar un enlace específico a tarea',
'Task link creation or modification' => 'Creación o modificación de enlace a tarea',
'Milestone' => 'Hito',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Compartido',
'Owner' => 'Dueño',
'Unread notifications' => 'Notificaciones sin leer',
- 'My filters' => 'Mis filtros',
'Notification methods:' => 'Métodos de notificación',
'Import tasks from CSV file' => 'Importar tareas desde archivo CSV',
'Unable to read your file' => 'No es posible leer el archivo',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Los nombres de usuario deben ser únicos y contener sólo minúsculas',
'Passwords will be encrypted if present' => 'Las contraseñas serán cifradas si es que existen',
'%s attached a new file to the task %s' => '%s adjuntó un nuevo archivo a la tarea %s',
+ 'Link type' => 'Tipo de enlace',
'Assign automatically a category based on a link' => 'Asignar una categoría automáticamente basado en un enlace',
'BAM - Konvertible Mark' => 'BAM - marco convertible',
'Assignee Username' => 'Nombre de usuario del concesionario',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Cerrar tarea cuando no haya actividad',
'Duration in days' => 'Duración en días',
'Send email when there is no activity on a task' => 'Enviar correo cuando no haya actividad en una tarea',
- 'List of external links' => 'Lista de enlaces externos',
'Unable to fetch link information.' => 'No es posible obtener información sobre el enlace',
'Daily background job for tasks' => 'Tarea de fondo diaria para las tareas',
'Auto' => 'Automático',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Enlace externo',
'Copy and paste your link here...' => 'Copia y pega tu enlace aquí...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'No existe un enlace externo por el momento',
'Internal links' => 'Enlaces internos',
- 'There is no internal link for the moment.' => 'No existe un enlace interno por el momento',
'Assign to me' => 'Asignar a mí',
'Me' => 'Yo',
'Do not duplicate anything' => 'No duplicar nada',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Administración de usuarios',
'Groups management' => 'Administración de grupos',
'Create from another project' => 'Crear de otro proyecto',
- 'There is no subtask at the moment.' => 'No existe subtarea por el momento',
'open' => 'abierto',
'closed' => 'cerrado',
'Priority:' => 'Prioridad',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Iniciado',
'Moved:' => 'Movido',
'Task #%d' => 'Tarea #%d',
- 'Sub-tasks' => 'Subtareas',
'Date and time format' => 'Formato de hora y fecha',
'Time format' => 'Formato de hora',
'Start date: ' => 'Fecha de inicio',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Usar filtros',
'Category filters' => 'Categoría y filtros',
'Upload a file' => 'Subir archivo',
- 'There is no attachment at the moment.' => 'No existe ningún adjunto por el momento',
'View file' => 'Ver archivo',
'Last activity' => 'Última actividad',
'Change subtask position' => 'Cambiar posición de la subtarea',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'No hay ninguna acción en este momento.',
'Import actions from another project' => 'Importar acciones de otro proyecto',
'There is no available project.' => 'No hay proyectos disponibles.',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 48b6d659..f30b7b4c 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Kaikki projektit',
'Add a new column' => 'Lisää uusi sarake',
'Title' => 'Nimi',
- 'Nobody assigned' => 'Ei suorittajaa',
'Assigned to %s' => 'Tekijä: %s',
'Remove a column' => 'Poista sarake',
'Remove a column from a board' => 'Poista sarake taulusta',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Tehtävien määrä',
'User' => 'Käyttäjät',
'Comments' => 'Kommentit',
- 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla',
'Leave a comment' => 'Lisää kommentti',
'Comment is required' => 'Kommentti vaaditaan',
'Leave a description' => 'Lisää kuvaus',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Ajan seuranta:',
'New sub-task' => 'Uusi alitehtävä',
'New attachment added "%s"' => 'Uusi liite lisätty "%s"',
- 'Comment updated' => 'Kommentti päivitetty',
'New comment posted by %s' => '%s lisäsi uuden kommentin',
// 'New attachment' => '',
// 'New comment' => '',
+ 'Comment updated' => 'Kommentti päivitetty',
// 'New subtask' => '',
// 'Subtask updated' => '',
// 'Task updated' => '',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-muoto on aina hyväksytty, esimerkiksi %s ja %s',
'New private project' => 'Uusi yksityinen projekti',
'This project is private' => 'Tämä projekti on yksityinen',
- 'Type here to create a new sub-task' => 'Kirjoita tähän luodaksesi uuden alitehtävän',
'Add' => 'Lisää',
'Start date' => 'Aloituspäivä',
'Time estimated' => 'Arvioitu aika',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Päivittäisen yhteenvedon vienti kohteeseen "%s"',
'Exports' => 'Viennit',
'This export contains the number of tasks per column grouped per day.' => 'Tämä tiedosto sisältää tehtäviä sarakkeisiin päiväkohtaisesti ryhmilteltyinä',
- 'Nothing to preview...' => 'Ei esikatselua...',
- 'Preview' => 'Ei esikatselua',
- 'Write' => 'Kirjoita',
'Active swimlanes' => 'Aktiiviset kaistat',
'Add a new swimlane' => 'Lisää uusi kaista',
'Change default swimlane' => 'Vaihda oletuskaistaa',
@@ -539,7 +533,6 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
- // 'Add a link' => '',
// 'Add a new link' => '',
// 'Do you really want to remove this link: "%s"?' => '',
// 'Do you really want to remove this link with task #%d?' => '',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index ec8a36bd..ed4638cd 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Tous les projets',
'Add a new column' => 'Ajouter une nouvelle colonne',
'Title' => 'Titre',
- 'Nobody assigned' => 'Personne assigné',
'Assigned to %s' => 'Assigné à %s',
'Remove a column' => 'Supprimer une colonne',
'Remove a column from a board' => 'Supprimer une colonne d\'un tableau',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Nombre de tâches',
'User' => 'Utilisateur',
'Comments' => 'Commentaires',
- 'Write your text in Markdown' => 'Écrivez votre texte en Markdown',
'Leave a comment' => 'Laissez un commentaire',
'Comment is required' => 'Le commentaire est obligatoire',
'Leave a description' => 'Laissez une description',
@@ -329,14 +327,12 @@ return array(
'Time tracking:' => 'Gestion du temps :',
'New sub-task' => 'Nouvelle sous-tâche',
'New attachment added "%s"' => 'Nouvelle pièce-jointe ajoutée « %s »',
- 'Comment updated' => 'Commentaire ajouté',
'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »',
'New attachment' => 'Nouveau document',
'New comment' => 'Nouveau commentaire',
'Comment updated' => 'Commentaire mis à jour',
'New subtask' => 'Nouvelle sous-tâche',
'Subtask updated' => 'Sous-tâche mise à jour',
- 'New task' => 'Nouvelle tâche',
'Task updated' => 'Tâche mise à jour',
'Task closed' => 'Tâche fermée',
'Task opened' => 'Tâche ouverte',
@@ -434,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Le format ISO est toujours accepté, exemple : « %s » et « %s »',
'New private project' => 'Nouveau projet privé',
'This project is private' => 'Ce projet est privé',
- 'Type here to create a new sub-task' => 'Créer une sous-tâche en écrivant le titre ici',
'Add' => 'Ajouter',
'Start date' => 'Date de début',
'Time estimated' => 'Temps estimé',
@@ -485,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export du résumé quotidien du projet pour « %s »',
'Exports' => 'Exports',
'This export contains the number of tasks per column grouped per day.' => 'Cet export contient le nombre de tâches par colonne groupé par jour.',
- 'Nothing to preview...' => 'Rien à prévisualiser...',
- 'Preview' => 'Prévisualiser',
- 'Write' => 'Écrire',
'Active swimlanes' => 'Swimlanes actives',
'Add a new swimlane' => 'Ajouter une nouvelle swimlane',
'Change default swimlane' => 'Modifier la swimlane par défaut',
@@ -541,7 +533,6 @@ return array(
'Task age in days' => 'Âge de la tâche en jours',
'Days in this column' => 'Jours dans cette colonne',
'%dd' => '%dj',
- 'Add a link' => 'Ajouter un lien',
'Add a new link' => 'Ajouter un nouveau lien',
'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche n°%d ?',
@@ -751,8 +742,6 @@ return array(
'My activity stream' => 'Mon flux d\'activité',
'My calendar' => 'Mon agenda',
'Search tasks' => 'Rechercher des tâches',
- 'Back to the calendar' => 'Retour au calendrier',
- 'Filters' => 'Filtres',
'Reset filters' => 'Réinitialiser les filtres',
'My tasks due tomorrow' => 'Mes tâches qui arrivent à échéance demain',
'Tasks due today' => 'Tâches qui arrivent à échéance aujourd\'hui',
@@ -852,7 +841,6 @@ return array(
'End date:' => 'Date de fin :',
'There is no start date or end date for this project.' => 'Il n\'y a pas de date de début ou de date de fin pour ce projet.',
'Projects Gantt chart' => 'Diagramme de Gantt des projets',
- 'Link type' => 'Type de lien',
'Change task color when using a specific task link' => 'Changer la couleur de la tâche lorsqu\'un lien spécifique est utilisé',
'Task link creation or modification' => 'Création ou modification d\'un lien sur une tâche',
'Milestone' => 'Étape importante',
@@ -904,7 +892,6 @@ return array(
'Shared' => 'Partagé',
'Owner' => 'Propriétaire',
'Unread notifications' => 'Notifications non lus',
- 'My filters' => 'Mes filtres',
'Notification methods:' => 'Méthodes de notifications :',
'Import tasks from CSV file' => 'Importer les tâches depuis un fichier CSV',
'Unable to read your file' => 'Impossible de lire votre fichier',
@@ -1052,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Fermer une tâche sans activité',
'Duration in days' => 'Durée en jours',
'Send email when there is no activity on a task' => 'Envoyer un email lorsqu\'il n\'y a pas d\'activité sur une tâche',
- 'List of external links' => 'Liste des liens externes',
'Unable to fetch link information.' => 'Impossible de récupérer les informations sur le lien.',
'Daily background job for tasks' => 'Tâche planifiée quotidienne pour les tâches',
'Auto' => 'Auto',
@@ -1070,9 +1056,7 @@ return array(
'External link' => 'Lien externe',
'Copy and paste your link here...' => 'Copier-coller vôtre lien ici...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'Il n\'y a pas de lien externe pour le moment.',
'Internal links' => 'Liens internes',
- 'There is no internal link for the moment.' => 'Il n\'y a pas de lien interne pour le moment.',
'Assign to me' => 'Assigner à moi',
'Me' => 'Moi',
'Do not duplicate anything' => 'Ne rien dupliquer',
@@ -1080,7 +1064,6 @@ return array(
'Users management' => 'Gestion des utilisateurs',
'Groups management' => 'Gestion des groupes',
'Create from another project' => 'Créer depuis un autre projet',
- 'There is no subtask at the moment.' => 'Il n\'y a aucune sous-tâche pour le moment.',
'open' => 'ouvert',
'closed' => 'fermé',
'Priority:' => 'Priorité :',
@@ -1099,7 +1082,6 @@ return array(
'Started:' => 'Commençé le :',
'Moved:' => 'Déplacé le : ',
'Task #%d' => 'Tâche n°%d',
- 'Sub-tasks' => 'Sous-tâches',
'Date and time format' => 'Format de la date et de l\'heure',
'Time format' => 'Format de l\'heure',
'Start date: ' => 'Date de début : ',
@@ -1140,7 +1122,6 @@ return array(
'User filters' => 'Filtres des utilisateurs',
'Category filters' => 'Filtres des catégories',
'Upload a file' => 'Uploader un fichier',
- 'There is no attachment at the moment.' => 'Il n\'y a aucune pièce-jointe pour le moment.',
'View file' => 'Voir le fichier',
'Last activity' => 'Dernières activités',
'Change subtask position' => 'Changer la position de la sous-tâche',
@@ -1154,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'Il n\'y a aucune action pour le moment.',
'Import actions from another project' => 'Importer les actions depuis un autre projet',
'There is no available project.' => 'Il n\'y a pas de projet disponible.',
+ 'Local File' => 'Fichier local',
+ 'Configuration' => 'Configuration',
+ 'PHP version:' => 'Version de PHP :',
+ 'PHP SAPI:' => 'PHP SAPI :',
+ 'OS version:' => 'Version du système d\'exploitation :',
+ 'Database version:' => 'Version de la base de donnée :',
+ 'Browser:' => 'Navigateur web :',
+ 'Task view' => 'Vue détaillée d\'une tâche',
+ 'Edit task' => 'Modifier la tâche',
+ 'Edit description' => 'Modifier la description',
+ 'New internal link' => 'Nouveau lien interne',
+ 'Display list of keyboard shortcuts' => 'Afficher la liste des raccourcis claviers',
+ 'Menu' => 'Menu',
+ 'Set start date' => 'Définir la date de début',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Uploader mon image d\'avatar',
+ 'Remove my image' => 'Supprimer mon image',
+ 'The OAuth2 state parameter is invalid' => 'Le paramètre "state" de OAuth2 est invalide',
+ 'User not found.' => 'Utilisateur introuvable.',
+ 'Search in activity stream' => 'Chercher dans le flux d\'activité',
+ 'My activities' => 'Mes activités',
+ 'Activity until yesterday' => 'Activités jusqu\'à hier',
+ 'Activity until today' => 'Activités jusqu\'à aujourd\'hui',
+ 'Search by creator: ' => 'Rechercher par créateur : ',
+ 'Search by creation date: ' => 'Rechercher par date de création : ',
+ 'Search by task status: ' => 'Rechercher par le statut des tâches : ',
+ 'Search by task title: ' => 'Rechercher par le titre des tâches : ',
+ 'Activity stream search' => 'Recherche dans le flux d\'activité',
+ 'Projects where "%s" is manager' => 'Projets où « %s » est gestionnaire',
+ 'Projects where "%s" is member' => 'Projets où « %s » est membre du projet',
+ 'Open tasks assigned to "%s"' => 'Tâches ouvertes assignées à « %s »',
+ 'Closed tasks assigned to "%s"' => 'Tâches fermées assignées à « %s »',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index 53e8ace5..394f89a0 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Minden projekt',
'Add a new column' => 'Új oszlop',
'Title' => 'Cím',
- 'Nobody assigned' => 'Nincs felelős',
'Assigned to %s' => 'Felelős: %s',
'Remove a column' => 'Oszlop törlése',
'Remove a column from a board' => 'Oszlop törlése a tábláról',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Feladatok száma',
'User' => 'Felhasználó',
'Comments' => 'Hozzászólások',
- 'Write your text in Markdown' => 'Írja be a szöveget Markdown szintaxissal',
'Leave a comment' => 'Írjon hozzászólást ...',
'Comment is required' => 'A hozzászólás mező kötelező',
'Leave a description' => 'Írjon leírást ...',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Idő követés:',
'New sub-task' => 'Új részfeladat',
'New attachment added "%s"' => 'Új melléklet "%s" hozzáadva.',
- 'Comment updated' => 'Megjegyzés frissítve',
'New comment posted by %s' => 'Új megjegyzés %s',
'New attachment' => 'Új melléklet',
'New comment' => 'Új megjegyzés',
+ 'Comment updated' => 'Megjegyzés frissítve',
'New subtask' => 'Új részfeladat',
'Subtask updated' => 'Részfeladat frissítve',
'Task updated' => 'Feladat frissítve',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formátum mindig elfogadott, pl: "%s" és "%s"',
'New private project' => 'Új privát projekt',
'This project is private' => 'Ez egy privát projekt',
- 'Type here to create a new sub-task' => 'Ide írva létrehozhat egy új részfeladatot',
'Add' => 'Hozzáadás',
'Start date' => 'Kezdés dátuma',
'Time estimated' => 'Becsült időtartam',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása: %s',
'Exports' => 'Exportálások',
'This export contains the number of tasks per column grouped per day.' => 'Ez az export tartalmazza a feladatok számát oszloponként összesítve, napokra lebontva.',
- 'Nothing to preview...' => 'Nincs semmi az előnézetben ...',
- 'Preview' => 'Előnézet',
- 'Write' => 'Szerkesztés',
'Active swimlanes' => 'Aktív folyamatok',
'Add a new swimlane' => 'Új folyamat',
'Change default swimlane' => 'Alapértelmezett folyamat változtatás',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Feladat életkora napokban',
'Days in this column' => 'Napok ebben az oszlopban',
'%dd' => '%dd',
- 'Add a link' => 'Hivatkozás hozzáadása',
'Add a new link' => 'Új hivatkozás hozzáadása',
'Do you really want to remove this link: "%s"?' => 'Biztos törölni akarja a hivatkozást: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Biztos törölni akarja a(z) #%d. feladatra mutató hivatkozást?',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php
index b4b8dcd4..bd1dd684 100644
--- a/app/Locale/id_ID/translations.php
+++ b/app/Locale/id_ID/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Semua proyek',
'Add a new column' => 'Tambah kolom baru',
'Title' => 'Judul',
- 'Nobody assigned' => 'Tidak ada yang ditugaskan',
'Assigned to %s' => 'Ditugaskan ke %s',
'Remove a column' => 'Hapus kolom',
'Remove a column from a board' => 'Hapus kolom dari papan',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Jumlah tugas',
'User' => 'Pengguna',
'Comments' => 'Komentar',
- 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown',
'Leave a comment' => 'Tinggalkan komentar',
'Comment is required' => 'Komentar diperlukan',
'Leave a description' => 'Tinggalkan deskripsi',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Pelacakan waktu :',
'New sub-task' => 'Sub-tugas baru',
'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »',
- 'Comment updated' => 'Komentar diperbaharui',
'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »',
'New attachment' => 'Lampirkan baru',
'New comment' => 'Komentar baru',
+ 'Comment updated' => 'Komentar diperbaharui',
'New subtask' => 'Sub-tugas baru',
'Subtask updated' => 'Sub-tugas diperbaharui',
'Task updated' => 'Tugas diperbaharui',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalu diterima, contoh : « %s » et « %s »',
'New private project' => 'Proyek pribadi baru',
'This project is private' => 'Proyek ini adalah pribadi',
- 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru',
'Add' => 'Tambah',
'Start date' => 'Tanggal mulai',
'Time estimated' => 'Perkiraan waktu',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Ekspor ringkasan proyek harian untuk « %s »',
'Exports' => 'Ekspor',
'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.',
- 'Nothing to preview...' => 'Tidak ada yang dapat dilihat...',
- 'Preview' => 'Preview',
- 'Write' => 'Tulis',
'Active swimlanes' => 'Swimlanes aktif',
'Add a new swimlane' => 'Tambah swimlane baru',
'Change default swimlane' => 'Modifikasi standar swimlane',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Usia tugas dalam hari',
'Days in this column' => 'Hari dalam kolom ini',
'%dd' => '%dj',
- 'Add a link' => 'Menambahkan tautan',
'Add a new link' => 'Tambah tautan baru',
'Do you really want to remove this link: "%s"?' => 'Apakah anda yakin akan menghapus tautan ini : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Apakah anda yakin akan menghapus tautan ini dengan tugas n°%d ?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Aliran kegiatan saya',
'My calendar' => 'Kalender saya',
'Search tasks' => 'Cari tugas',
- 'Back to the calendar' => 'Kembali ke kalender',
- 'Filters' => 'Filter',
'Reset filters' => 'Reset ulang filter',
'My tasks due tomorrow' => 'Tugas saya yang berakhir besok',
'Tasks due today' => 'Tugas yang berakhir hari ini',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Waktu berakhir :',
'There is no start date or end date for this project.' => 'Tidak ada waktu mulai atau waktu berakhir untuk proyek ini',
'Projects Gantt chart' => 'Proyek grafik Gantt',
- 'Link type' => 'Tipe tautan',
'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan tautan tugas yang spesifik',
'Task link creation or modification' => 'Tautan pembuatan atau modifikasi tugas ',
'Milestone' => 'Milestone',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ 'Link type' => 'Tipe tautan',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index bba629cb..cee1c16a 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Tutti i progetti',
'Add a new column' => 'Aggiungi una nuova colonna',
'Title' => 'Titolo',
- 'Nobody assigned' => 'Nessuno assegnato',
'Assigned to %s' => 'Assegnato a %s',
'Remove a column' => 'Cancella questa colonna',
'Remove a column from a board' => 'Cancella una colonna da una bacheca',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Numero di task',
'User' => 'Utente',
'Comments' => 'Commenti',
- 'Write your text in Markdown' => 'Scrivi il testo in Markdown',
'Leave a comment' => 'Lascia un commento',
'Comment is required' => 'Si richiede un commento',
'Leave a description' => 'Lascia una descrizione',
@@ -329,10 +327,10 @@ return array(
// 'Time tracking:' => '',
'New sub-task' => 'Nuovo sotto-task',
'New attachment added "%s"' => 'Nuovo allegato aggiunto "%s"',
- 'Comment updated' => 'Commento aggiornato',
'New comment posted by %s' => 'Nuovo commento aggiunto da "%s"',
'New attachment' => 'Nuovo allegato',
'New comment' => 'Nuovo commento',
+ 'Comment updated' => 'Commento aggiornato',
'New subtask' => 'Nuovo sotto-task',
'Subtask updated' => 'Sotto-task aggiornato',
'Task updated' => 'Task aggiornato',
@@ -432,13 +430,12 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Il formato ISO è sempre accettato, esempio: "%s" e "%s"',
'New private project' => 'Nuovo progetto privato',
'This project is private' => 'Questo progetto è privato',
- 'Type here to create a new sub-task' => 'Scrivi qui per creare un sotto-task',
'Add' => 'Aggiungi',
'Start date' => 'Data di inizio',
'Time estimated' => 'Tempo stimato',
'There is nothing assigned to you.' => 'Non c\'è nulla assegnato a te.',
'My tasks' => 'I miei task',
- 'Activity stream' => 'Flusso di attività',
+ 'Activity stream' => 'Flusso attività',
'Dashboard' => 'Bacheca',
'Confirmation' => 'Conferma',
'Allow everybody to access to this project' => 'Abilita tutti ad accedere a questo progetto',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export del sommario giornaliero del progetto per "%s"',
'Exports' => 'Esporta',
'This export contains the number of tasks per column grouped per day.' => 'Questo export contiene il numero di task per colonna raggruppati per giorno',
- 'Nothing to preview...' => 'Nessuna anteprima...',
- 'Preview' => 'Anteprima',
- 'Write' => 'Scrivi',
'Active swimlanes' => 'Corsie attive',
'Add a new swimlane' => 'Aggiungi una corsia',
'Change default swimlane' => 'Cambia la corsia predefinita',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Anzianità del task in giorni',
'Days in this column' => 'Giorni in questa colonna',
'%dd' => '%dg',
- 'Add a link' => 'Aggiungi una relazione',
'Add a new link' => 'Aggiungi una nuova relazione',
'Do you really want to remove this link: "%s"?' => 'Vuoi davvero rimuovere la seguente relazione: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Vuoi davvero rimuovere questa relazione dal task #%d?',
@@ -746,11 +739,9 @@ return array(
'Start timer' => 'Avvia il timer',
'Add project member' => 'Aggiungi un membro di progetto',
'Enable notifications' => 'Abilita notifiche',
- 'My activity stream' => 'Il mio flusso di attività',
+ 'My activity stream' => 'Il mio flusso attività',
'My calendar' => 'Il mio calendario',
'Search tasks' => 'Ricerca task',
- 'Back to the calendar' => 'Torna al calendario',
- 'Filters' => 'Filtri',
'Reset filters' => 'Annulla filtri',
'My tasks due tomorrow' => 'I miei task da completare per domani',
'Tasks due today' => 'Task da completare oggi',
@@ -771,7 +762,7 @@ return array(
'Keyboard shortcut: "%s"' => 'Scorciatoia da tastiera: "%s"',
'List' => 'Lista',
'Filter' => 'Filtro',
- 'Advanced search' => 'Riceca avanzata',
+ 'Advanced search' => 'Ricerca avanzata',
'Example of query: ' => 'Esempio di query: ',
'Search by project: ' => 'Ricerca per progetto: ',
'Search by column: ' => 'Ricerca per colonna: ',
@@ -842,7 +833,7 @@ return array(
'Users overview' => 'Panoramica utenti',
'Members' => 'Membri',
'Shared project' => 'Progetto condiviso',
- 'Project managers' => 'Manager del progetto',
+ 'Project managers' => 'Manager di progetto',
'Gantt chart for all projects' => 'Grafico Gantt per tutti i progetti',
'Projects list' => 'Elenco progetti',
'Gantt chart for this project' => 'Grafico Gantt per questo progetto',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Data di fine:',
'There is no start date or end date for this project.' => 'Non è prevista una data di inzio o fine per questo progetto.',
'Projects Gantt chart' => 'Grafico Gantt dei progetti',
- 'Link type' => 'Tipo di link',
'Change task color when using a specific task link' => 'Cambia colore del task quando si un utilizza una determinata relazione di task',
'Task link creation or modification' => 'Creazione o modifica di relazione di task',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Condiviso',
'Owner' => 'Proprietario',
'Unread notifications' => 'Notifiche non lette',
- 'My filters' => 'I miei filtri',
'Notification methods:' => 'Metodi di notifica',
'Import tasks from CSV file' => 'Importa task da file CSV',
'Unable to read your file' => 'Impossibile leggere il file',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'I nomi utente devono essere in minuscolo e univoci',
'Passwords will be encrypted if present' => 'Se presenti, le password verranno criptate',
'%s attached a new file to the task %s' => '%s ha allegato un nuovo file al task %s',
+ 'Link type' => 'Tipo di link',
'Assign automatically a category based on a link' => 'Assegna automaticamente una categoria sulla base di una relazione',
'BAM - Konvertible Mark' => 'BAM - Marco bosniaco',
'Assignee Username' => 'Nome utente dell\'assegnatario',
@@ -990,7 +980,7 @@ return array(
'Group Name' => 'Nome del gruppo',
'Enter group name...' => 'Inserisci il nome del gruppo...',
'Role:' => 'Ruolo:',
- 'Project members' => 'Membri del progetto',
+ 'Project members' => 'Membri di progetto',
'Compare hours for "%s"' => 'Confronta le ore per "%s"',
'%s mentioned you in the task #%d' => '%s ti ha menzionato nel task #%d',
'%s mentioned you in a comment on the task #%d' => '%s ti ha menzionato in un commento del task #%d',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Chiudi un task quando non c\'è nessuna attività',
'Duration in days' => 'Durata in giorni',
'Send email when there is no activity on a task' => 'Invia un\'email quando non c\'è attività sul task',
- 'List of external links' => 'Elenco dei link esterni',
'Unable to fetch link information.' => 'Impossibile recuperare informazioni sul link.',
'Daily background job for tasks' => 'Job giornaliero in background per i task',
// 'Auto' => '',
@@ -1067,17 +1056,14 @@ return array(
'External link' => 'Link esterno',
'Copy and paste your link here...' => 'Copia e incolla il tuo link qui...',
// 'URL' => '',
- 'There is no external link for the moment.' => 'Nessun link esterno presente al momento.',
'Internal links' => 'Link interni',
- 'There is no internal link for the moment.' => 'Nessun link interno presente al momento.',
'Assign to me' => 'Assegna a me',
- // 'Me' => '',
+ 'Me' => 'Me stesso',
'Do not duplicate anything' => 'Non duplicare nulla',
'Projects management' => 'Gestione progetti',
'Users management' => 'Gestione utenti',
'Groups management' => 'Gestione gruppi',
'Create from another project' => 'Crea da un\'altro progetto',
- 'There is no subtask at the moment.' => 'Nessun sotto-task presente al momento.',
'open' => 'aperto',
'closed' => 'chiuso',
'Priority:' => 'Priorità:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Iniziato:',
'Moved:' => 'Spostato:',
// 'Task #%d' => '',
- 'Sub-tasks' => 'Sotto-task',
'Date and time format' => 'Formato data e ora',
'Time format' => 'Formato ora',
'Start date: ' => 'Data di inizio: ',
@@ -1137,18 +1122,49 @@ return array(
'User filters' => 'Filtri utente',
'Category filters' => 'Filtri di categorie',
'Upload a file' => 'Carica un file',
- 'There is no attachment at the moment.' => 'Nessun allegato presente.',
'View file' => 'Visualizza file',
'Last activity' => 'Attività recente',
'Change subtask position' => 'Cambia la posizione del sotto-task',
'This value must be greater than %d' => 'Questo valore deve essere magiore di %d',
'Another swimlane with the same name exists in the project' => 'Un\'altra corsia con lo stesso nome è già esistente in questo progetto',
'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Esempio: http://example.kanboard.net/ (usato per generare URL assolute)',
- // 'Actions duplicated successfully.' => '',
- // 'Unable to duplicate actions.' => '',
- // 'Add a new action' => '',
- // 'Import from another project' => '',
- // 'There is no action at the moment.' => '',
- // 'Import actions from another project' => '',
- // 'There is no available project.' => '',
+ 'Actions duplicated successfully.' => 'Azioni duplicate con successo.',
+ 'Unable to duplicate actions.' => 'Impossibile duplicare le azioni.',
+ 'Add a new action' => 'Aggiungi una nuova azione',
+ 'Import from another project' => 'Importa da un altro progetto',
+ 'There is no action at the moment.' => 'Nessuna azione disponibile al momento.',
+ 'Import actions from another project' => 'Importa azioni da un altro progetto',
+ 'There is no available project.' => 'Nessun progetto disponibile.',
+ 'Local File' => 'File locale',
+ 'Configuration' => 'Configurazione',
+ 'PHP version:' => 'Versione PHP:',
+ // 'PHP SAPI:' => '',
+ 'OS version:' => 'Versione OS:',
+ 'Database version:' => 'Versione database:',
+ // 'Browser:' => '',
+ 'Task view' => 'Vista dei task',
+ 'Edit task' => 'Modifica task',
+ 'Edit description' => 'Modifica descrizione',
+ 'New internal link' => 'Nuovo link interno',
+ 'Display list of keyboard shortcuts' => 'Mostra una lista di scorciatoie da tastiera',
+ // 'Menu' => '',
+ 'Set start date' => 'Imposta la data di inzio',
+ // 'Avatar' => '',
+ 'Upload my avatar image' => 'Carica l\'immagine del mio avatar',
+ 'Remove my image' => 'Rimuovi la mia immagine',
+ 'The OAuth2 state parameter is invalid' => 'Il parametro di stato OAuth2 non è valido.',
+ 'User not found.' => 'Utente non trovato.',
+ 'Search in activity stream' => 'Ricerca nel mio flusso attività',
+ 'My activities' => 'Le mie attività',
+ 'Activity until yesterday' => 'Attività ad oggi',
+ 'Activity until today' => 'Attività fino a ieri',
+ 'Search by creator: ' => 'Ricerca per creatore: ',
+ 'Search by creation date: ' => 'Ricerca per data di creazione: ',
+ 'Search by task status: ' => 'Ricerca per stato del task: ',
+ 'Search by task title: ' => 'Ricerca per titolo del task: ',
+ 'Activity stream search' => 'Ricerca nel flusso attività',
+ 'Projects where "%s" is manager' => 'Progetti all\'interno dei quali "%s" è manager',
+ 'Projects where "%s" is member' => 'Progetti all\'interno dei quali "%s" è membro',
+ 'Open tasks assigned to "%s"' => 'Task aperti assegnati a "%s"',
+ 'Closed tasks assigned to "%s"' => 'Task chiusi assegnati a "%s"',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index e4b41c93..89769edd 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'すべてのプロジェクト',
'Add a new column' => 'カラムの追加',
'Title' => 'タイトル',
- 'Nobody assigned' => '担当なし',
'Assigned to %s' => '%sが担当',
'Remove a column' => 'カラムの削除',
'Remove a column from a board' => 'ボードからカラムの削除',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'タスク数',
'User' => 'ユーザ',
'Comments' => 'コメント',
- 'Write your text in Markdown' => 'Markdown 記法で書く',
'Leave a comment' => 'コメントを書く',
'Comment is required' => 'コメントを入力してください',
'Leave a description' => '説明を書く',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => '時間計測:',
'New sub-task' => '新しいサブタスク',
'New attachment added "%s"' => '添付ファイル「%s」が追加されました',
- 'Comment updated' => 'コメントが更新されました',
'New comment posted by %s' => '「%s」の新しいコメントが追加されました',
'New attachment' => '新しい添付ファイル',
'New comment' => '新しいコメント',
+ 'Comment updated' => 'コメントが更新されました',
'New subtask' => '新しいサブタスク',
'Subtask updated' => 'サブタスクの更新',
'Task updated' => 'タスクの更新',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO フォーマットが入力できます(例: %s または %s)',
'New private project' => '非公開プロジェクトを作る',
'This project is private' => 'このプロジェクトは非公開です',
- 'Type here to create a new sub-task' => 'サブタスクを追加するにはここに入力してください',
'Add' => '追加',
'Start date' => '開始時間',
'Time estimated' => '予想時間',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => '「%s」の日時プロジェクトサマリーの出力',
'Exports' => '出力',
'This export contains the number of tasks per column grouped per day.' => 'この出力は日時のカラムごとのタスク数を集計したものです',
- 'Nothing to preview...' => 'プレビューがありません',
- 'Preview' => 'プレビュー',
- 'Write' => '書く',
'Active swimlanes' => 'アクティブなスイムレーン',
'Add a new swimlane' => '新しいスイムレーン',
'Change default swimlane' => 'デフォルトスイムレーンの変更',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'タスクの経過日数',
'Days in this column' => 'カラムでの経過日数',
'%dd' => '%d 日',
- 'Add a link' => 'リンクの追加',
'Add a new link' => '新しいリンクの追加',
'Do you really want to remove this link: "%s"?' => 'リンク「%s」を本当に削除しますか?',
'Do you really want to remove this link with task #%d?' => 'このリンクとタスク#%dを削除しますか?',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php
new file mode 100644
index 00000000..ed9e3b86
--- /dev/null
+++ b/app/Locale/ko_KR/translations.php
@@ -0,0 +1,1170 @@
+<?php
+
+return array(
+ // 'number.decimals_separator' => '',
+ // 'number.thousands_separator' => '',
+ 'None' => '없음',
+ 'edit' => '수정',
+ 'Edit' => '수정',
+ 'remove' => '삭제',
+ 'Remove' => '삭제',
+ 'Yes' => '예',
+ 'No' => '아니오',
+ 'cancel' => '취소',
+ 'or' => '또는',
+ 'Yellow' => '노랑',
+ 'Blue' => '파랑',
+ 'Green' => '초록',
+ 'Purple' => '보라',
+ 'Red' => '빨강',
+ 'Orange' => '주황',
+ 'Grey' => '회색',
+ 'Brown' => '브라운',
+ // 'Deep Orange' => '',
+ // 'Dark Grey' => '',
+ // 'Pink' => '',
+ // 'Teal' => '',
+ // 'Cyan' => '',
+ // 'Lime' => '',
+ // 'Light Green' => '',
+ // 'Amber' => '',
+ 'Save' => '저장',
+ 'Login' => '로그인',
+ 'Official website:' => '공식 웹사이트:',
+ 'Unassigned' => '담당자 없음',
+ 'View this task' => '이 할일 보기',
+ 'Remove user' => '사용자 삭제',
+ 'Do you really want to remove this user: "%s"?' => '사용자 "%s"를 정말로 삭제하시겠습니까?',
+ 'New user' => '사용자를 추가하는 ',
+ 'All users' => '모든 사용자',
+ 'Username' => '사용자 이름',
+ 'Password' => '패스워드',
+ 'Administrator' => '관리자',
+ 'Sign in' => '로그인',
+ 'Users' => '사용자',
+ 'No user' => '사용자가 없습니다',
+ 'Forbidden' => '접근 거부',
+ 'Access Forbidden' => '접속이 거부되었습니다',
+ 'Edit user' => '사용자를 변경하는 ',
+ 'Logout' => '로그아웃',
+ 'Bad username or password' => '사용자 이름 또는 패스워드가 다릅니다.',
+ 'Edit project' => '프로젝트 수정',
+ 'Name' => '이름',
+ 'Projects' => '프로젝트',
+ 'No project' => '프로젝트가 없습니다',
+ 'Project' => '프로젝트',
+ 'Status' => '상태',
+ 'Tasks' => '할일',
+ 'Board' => '보드',
+ 'Actions' => 'Actions',
+ 'Inactive' => '무효',
+ 'Active' => '유효',
+ '%d tasks on the board' => '%d개의 할일',
+ '%d tasks in total' => '총 %d개의 할일',
+ 'Unable to update this board.' => '보드를 갱신할 수 없었습니다',
+ 'Edit board' => '보드를 변경하는 ',
+ 'Disable' => '비활성화',
+ 'Enable' => '유효하게 한다',
+ 'New project' => '새 프로젝트',
+ // 'Do you really want to remove this project: "%s"?' => '',
+ 'Remove project' => '프로젝트의 삭제',
+ // 'Edit the board for "%s"' => '',
+ 'All projects' => '모든 프로젝트',
+ 'Add a new column' => '칼럼의 추가',
+ 'Title' => '제목',
+ 'Assigned to %s' => '담당자 %s',
+ 'Remove a column' => '칼럼 삭제',
+ 'Remove a column from a board' => '보드에서 칼럼 삭제',
+ 'Unable to remove this column.' => '(※)컬럼을 삭제할 수 없었습니다.',
+ // 'Do you really want to remove this column: "%s"?' => '',
+ 'This action will REMOVE ALL TASKS associated to this column!' => '이 조작은 이 컬럼에 할당된 『 모든 할일을 삭제 』합니다!',
+ 'Settings' => '설정',
+ 'Application settings' => '애플리케이션의 설정',
+ 'Language' => '언어',
+ 'Webhook token:' => 'Webhook토큰:',
+ 'API token:' => 'API토큰:',
+ 'Database size:' => '데이터베이스의 사이즈:',
+ 'Download the database' => '데이터베이스의 다운로드',
+ 'Optimize the database' => '데이터베이스 최적화',
+ '(VACUUM command)' => '(VACUUM명령)',
+ '(Gzip compressed Sqlite file)' => '(GZip명령으로 압축된 Sqlite파일)',
+ 'Close a task' => '할일 마치기',
+ 'Edit a task' => '할일 수정',
+ 'Column' => '칼럼',
+ 'Color' => '색',
+ 'Assignee' => '담당자',
+ 'Create another task' => '다른 할일 추가',
+ 'New task' => '새로운 할일',
+ 'Open a task' => '할일 열기',
+ // 'Do you really want to open this task: "%s"?' => '',
+ 'Back to the board' => '보드로 돌아가기',
+ 'There is nobody assigned' => '담당자가 없습니다',
+ 'Column on the board:' => '칼럼:',
+ 'Close this task' => '할일 마치기',
+ 'Open this task' => '할일을 열다',
+ 'There is no description.' => '설명이 없다',
+ 'Add a new task' => '할일을 추가하는 ',
+ 'The username is required' => '사용자 이름이 필요합니다',
+ // 'The maximum length is %d characters' => '',
+ // 'The minimum length is %d characters' => '',
+ 'The password is required' => '패스워드가 필요합니다',
+ 'This value must be an integer' => '정수로 입력하세요',
+ 'The username must be unique' => '사용자 이름이 이미 사용되고 있습니다',
+ 'The user id is required' => '사용자 ID가 필요합니다',
+ 'Passwords don\'t match' => '패스워드가 일치하지 않습니다',
+ 'The confirmation is required' => '확인용 패스워드를 입력하세요',
+ 'The project is required' => '프로젝트가 필요합니다',
+ 'The id is required' => 'ID가 필요합니다',
+ 'The project id is required' => '프로젝트 ID가 필요합니다',
+ 'The project name is required' => '프로젝트 이름이 필요합니다',
+ 'The title is required' => '제목이 필요합니다',
+ 'Settings saved successfully.' => '설정을 저장하였습니다',
+ 'Unable to save your settings.' => '설정의 보존에 실패했습니다.',
+ 'Database optimization done.' => '데이터베이스 최적화가 끝났습니다.',
+ 'Your project have been created successfully.' => '프로젝트를 작성했습니다.',
+ 'Unable to create your project.' => '프로젝트의 작성에 실패했습니다.',
+ 'Project updated successfully.' => '프로젝트를 갱신했습니다.',
+ 'Unable to update this project.' => '프로젝트의 갱신에 실패했습니다.',
+ 'Unable to remove this project.' => '프로젝트의 삭제에 실패했습니다.',
+ 'Project removed successfully.' => '프로젝트를 삭제했습니다.',
+ 'Project activated successfully.' => '프로젝트를 유효로 했습니다.',
+ 'Unable to activate this project.' => '프로젝트의 유효하게 못했어요.',
+ 'Project disabled successfully.' => '프로젝트를 무효로 했습니다.',
+ 'Unable to disable this project.' => '프로젝트의 무효화할 수 없었습니다.',
+ 'Unable to open this task.' => '할일의 오픈에 실패했습니다.',
+ 'Task opened successfully.' => '할일을 오픈했습니다.',
+ 'Unable to close this task.' => '할일의 클로즈에 실패했습니다.',
+ 'Task closed successfully.' => '할일을 마쳤습니다.',
+ 'Unable to update your task.' => '할일의 갱신에 실패했습니다.',
+ 'Task updated successfully.' => '할일을 갱신했습니다.',
+ 'Unable to create your task.' => '할일의 추가에 실패했습니다.',
+ 'Task created successfully.' => '할일을 추가했습니다.',
+ 'User created successfully.' => '사용자를 추가했습니다.',
+ 'Unable to create your user.' => '사용자의 추가에 실패했습니다.',
+ 'User updated successfully.' => '사용자를 갱신했습니다.',
+ 'Unable to update your user.' => '사용자의 갱신에 실패했습니다.',
+ 'User removed successfully.' => '사용자를 삭제했습니다.',
+ 'Unable to remove this user.' => '사용자 삭제에 실패했습니다.',
+ 'Board updated successfully.' => '보드를 갱신했습니다.',
+ 'Ready' => '준비완료',
+ 'Backlog' => '요구사항',
+ 'Work in progress' => '진행중',
+ 'Done' => '완료',
+ 'Application version:' => '애플리케이션의 버전:',
+ 'Id' => 'ID',
+ '%d closed tasks' => '%d개의 마친 할일',
+ 'No task for this project' => '이 프로젝트에 할일이 없습니다',
+ 'Public link' => '공개 접속 링크',
+ 'Change assignee' => '담당자 변경',
+ 'Change assignee for the task "%s"' => '할일 "%s"의 담당자를 변경',
+ 'Timezone' => '시간대',
+ 'Sorry, I didn\'t find this information in my database!' => '데이터베이스에서 정보가 발견되지 않았습니다!',
+ 'Page not found' => '페이지가 발견되지 않는다',
+ 'Complexity' => '복잡도',
+ 'Task limit' => '할일 수 제한',
+ 'Task count' => '할일 수',
+ 'User' => '사용자',
+ 'Comments' => '댓글',
+ 'Leave a comment' => '댓글 남기기',
+ 'Comment is required' => '댓글을 입력하세요',
+ 'Leave a description' => '설명을 입력하세요',
+ 'Comment added successfully.' => '의견을 추가했습니다.',
+ 'Unable to create your comment.' => '댓글의 추가에 실패했습니다.',
+ 'Edit this task' => '할일 수정',
+ 'Due Date' => '마감일',
+ 'Invalid date' => '날짜가 무효입니다',
+ 'Automatic actions' => '자동액션 관리',
+ 'Your automatic action have been created successfully.' => '자동 액션을 작성했습니다.',
+ 'Unable to create your automatic action.' => '자동 액션의 작성에 실패했습니다.',
+ 'Remove an action' => '자동 액션의 삭제',
+ 'Unable to remove this action.' => '자동 액션의 삭제에 실패했습니다.',
+ 'Action removed successfully.' => '자동 액션의 삭제에 성공했어요.',
+ // 'Automatic actions for the project "%s"' => '',
+ 'Add an action' => '자동 액션 추가',
+ 'Event name' => '이벤트 이름',
+ 'Action name' => '액션 이름',
+ 'Action parameters' => '액션의 바로미터',
+ 'Action' => '액션',
+ 'Event' => '이벤트',
+ 'When the selected event occurs execute the corresponding action.' => '선택된 이벤트가 발생했을 때 대응하는 액션을 실행한다.',
+ 'Next step' => '다음 단계',
+ 'Define action parameters' => '액션의 바로미터',
+ // 'Do you really want to remove this action: "%s"?' => '',
+ 'Remove an automatic action' => '자동 액션의 삭제',
+ 'Assign the task to a specific user' => '할일 담당자를 할당',
+ 'Assign the task to the person who does the action' => '액션을 일으킨 사용자를 담당자이자',
+ 'Duplicate the task to another project' => ' 다른 프로젝트에 할일을 복제하는 ',
+ 'Move a task to another column' => '할일을 다른 칼럼에 이동하는 ',
+ 'Task modification' => '할일 변경',
+ 'Task creation' => '할일을 만들',
+ 'Closing a task' => '할일을 닫혔다',
+ 'Assign a color to a specific user' => '색을 사용자에 할당',
+ 'Column title' => '칼럼의 제목',
+ 'Position' => '위치',
+ 'Duplicate to another project' => '다른 프로젝트에 복사',
+ 'Duplicate' => '복사',
+ 'link' => '링크',
+ 'Comment updated successfully.' => '댓글을 갱신했습니다.',
+ 'Unable to update your comment.' => '댓글의 갱신에 실패했습니다.',
+ 'Remove a comment' => '댓글 삭제',
+ 'Comment removed successfully.' => '댓글을 삭제했습니다.',
+ 'Unable to remove this comment.' => '댓글의 삭제에 실패했습니다.',
+ 'Do you really want to remove this comment?' => '댓글을 삭제합니까?',
+ 'Current password for the user "%s"' => '사용자 "%s"의 현재 패스워드',
+ 'The current password is required' => '현재의 패스워드를 입력하세요',
+ 'Wrong password' => '패스워드가 다릅니다',
+ 'Unknown' => '불명',
+ 'Last logins' => '마지막 로그인',
+ 'Login date' => '로그인 일시',
+ 'Authentication method' => '인증 방법',
+ 'IP address' => 'IP 주소',
+ 'User agent' => '사용자 에이전트',
+ 'Persistent connections' => '세션',
+ 'No session.' => '세션 없음',
+ 'Expiration date' => '유효기간',
+ 'Remember Me' => '자동 로그인',
+ 'Creation date' => '작성일',
+ 'Everybody' => '모두',
+ 'Open' => '열림',
+ 'Closed' => '닫힘',
+ 'Search' => '검색',
+ 'Nothing found.' => '결과가 없습니다',
+ 'Due date' => '마감일',
+ 'Others formats accepted: %s and %s' => ' 다른 서식: %s 또는 %s',
+ 'Description' => '설명',
+ '%d comments' => '%d개의 댓글',
+ '%d comment' => '%d개의 댓글',
+ 'Email address invalid' => '메일 주소가 올바르지 않습니다.',
+ // 'Your external account is not linked anymore to your profile.' => '',
+ // 'Unable to unlink your external account.' => '',
+ // 'External authentication failed' => '',
+ // 'Your external account is linked to your profile successfully.' => '',
+ 'Email' => '이메일',
+ 'Task removed successfully.' => '할일을 삭제했습니다.',
+ 'Unable to remove this task.' => '할일 삭제에 실패했습니다.',
+ 'Remove a task' => '할일 삭제',
+ // 'Do you really want to remove this task: "%s"?' => '',
+ 'Assign automatically a color based on a category' => '카테고리에 바탕을 두고 색을 바꾸고',
+ 'Assign automatically a category based on a color' => '색에 바탕을 두고 카테고리를 바꾸었다',
+ 'Task creation or modification' => '할일의 작성 또는 변경',
+ 'Category' => '카테고리',
+ 'Category:' => '카테고리:',
+ 'Categories' => '카테고리',
+ 'Category not found.' => '카테고리가 발견되지 않습니다',
+ 'Your category have been created successfully.' => '카테고리를 작성했습니다.',
+ 'Unable to create your category.' => '카테고리의 작성에 실패했습니다.',
+ 'Your category have been updated successfully.' => '카테고리를 갱신했습니다.',
+ 'Unable to update your category.' => '카테고리의 갱신에 실패했습니다.',
+ 'Remove a category' => '카테고리의 삭제',
+ 'Category removed successfully.' => '카테고리를 삭제했습니다.',
+ 'Unable to remove this category.' => '카테고리를 삭제할 수 없었습니다.',
+ // 'Category modification for the project "%s"' => '',
+ 'Category Name' => '카테고리 이름',
+ 'Add a new category' => '카테고리의 추가',
+ // 'Do you really want to remove this category: "%s"?' => '',
+ 'All categories' => '모든 카테고리',
+ 'No category' => '카테고리 없음',
+ 'The name is required' => '이름을 입력하십시오',
+ 'Remove a file' => '파일 삭제',
+ 'Unable to remove this file.' => '파일 삭제에 실패했습니다.',
+ 'File removed successfully.' => '파일을 삭제했습니다.',
+ 'Attach a document' => '문서 첨부',
+ 'Do you really want to remove this file: "%s"?' => '파일 "%s" 을 삭제할까요?',
+ 'Attachments' => '첨부',
+ 'Edit the task' => '할일 수정',
+ 'Edit the description' => '설명 수정',
+ 'Add a comment' => '댓글 추가',
+ 'Edit a comment' => '댓글 수정',
+ 'Summary' => '개요',
+ 'Time tracking' => '시간 추적',
+ 'Estimate:' => '예측:',
+ 'Spent:' => '경과:',
+ 'Do you really want to remove this sub-task?' => '서브 할일을 삭제합니까?',
+ 'Remaining:' => '나머지:',
+ 'hours' => '시간',
+ 'spent' => '경과',
+ 'estimated' => '예측',
+ 'Sub-Tasks' => '서브 할일',
+ 'Add a sub-task' => '서브 할일 추가',
+ 'Original estimate' => '최초 예측시간',
+ 'Create another sub-task' => '다음 서브 할일 추가',
+ 'Time spent' => '경과시간',
+ 'Edit a sub-task' => '서브 할일을 변경하는 ',
+ 'Remove a sub-task' => '서브 할일을 삭제하는 ',
+ 'The time must be a numeric value' => '시간은 숫자로 입력하세요',
+ 'Todo' => '할일 예정',
+ 'In progress' => '할일 중',
+ 'Sub-task removed successfully.' => '서브 할일을 삭제했습니다.',
+ 'Unable to remove this sub-task.' => '서브 할일의 삭제가 실패했습니다.',
+ 'Sub-task updated successfully.' => '서브 할일을 갱신했습니다.',
+ 'Unable to update your sub-task.' => '서브 할일의 경신에 실패했습니다.',
+ 'Unable to create your sub-task.' => '서브 할일의 추가에 실패했습니다.',
+ 'Sub-task added successfully.' => '서브 할일을 추가했습니다.',
+ 'Maximum size: ' => '최대: ',
+ 'Unable to upload the file.' => '파일 업로드에 실패했습니다.',
+ 'Display another project' => '프로젝트 보기',
+ 'Created by %s' => '작성자 %s',
+ 'Tasks Export' => '할일 내보내기',
+ // 'Tasks exportation for "%s"' => '',
+ 'Start Date' => '시작일',
+ 'End Date' => '종료일',
+ 'Execute' => '실행',
+ 'Task Id' => '할일 ID',
+ 'Creator' => '작성자',
+ 'Modification date' => '변경 일',
+ 'Completion date' => '완료일',
+ 'Clone' => '복사',
+ 'Project cloned successfully.' => '프로젝트를 복제했습니다.',
+ 'Unable to clone this project.' => '프로젝트의 복제에 실패했습니다.',
+ 'Enable email notifications' => '이메일 알림 설정',
+ 'Task position:' => '할일 위치:',
+ // 'The task #%d have been opened.' => '',
+ // 'The task #%d have been closed.' => '',
+ 'Sub-task updated' => '서브 할일 갱신',
+ 'Title:' => '제목:',
+ 'Status:' => '상태:',
+ 'Assignee:' => '담당:',
+ 'Time tracking:' => '시간 계측:',
+ 'New sub-task' => '새로운 서브 할일',
+ // 'New attachment added "%s"' => '',
+ 'New comment posted by %s' => '"%s"님이 댓글을 추가하였습니다',
+ 'New attachment' => ' 새로운 첨부 파일',
+ 'New comment' => ' 새로운 댓글',
+ 'Comment updated' => '댓글가 갱신되었습니다',
+ 'New subtask' => ' 새로운 서브 할일',
+ 'Subtask updated' => '서브 할일 갱신',
+ 'Task updated' => '할일 업데이트',
+ 'Task closed' => '할일 마침',
+ 'Task opened' => '할일 시작',
+ 'I want to receive notifications only for those projects:' => '다음 프로젝트의 알림만 받겠습니다:',
+ 'view the task on Kanboard' => 'Kanboard에서 할일을 본다',
+ 'Public access' => '공개 접속 설정',
+ 'Active tasks' => '활성화된 할일',
+ 'Disable public access' => '공개 접속 비활성화',
+ 'Enable public access' => '공개 접속 활성화',
+ 'Public access disabled' => '공개 접속 불가',
+ // 'Do you really want to disable this project: "%s"?' => '',
+ // 'Do you really want to enable this project: "%s"?' => '',
+ 'Project activation' => '프로젝트의 액티베ー션',
+ 'Move the task to another project' => '할일별 프로젝트에 옮기',
+ 'Move to another project' => '다른 프로젝트로 이동',
+ 'Do you really want to duplicate this task?' => '할일을 복제합니까?',
+ 'Duplicate a task' => '할일 복사',
+ 'External accounts' => '외부 계정',
+ 'Account type' => '계정종류',
+ 'Local' => '로컬',
+ 'Remote' => '원격',
+ 'Enabled' => '활성화',
+ 'Disabled' => '비활성화',
+ 'Username:' => '사용자명',
+ 'Name:' => '이름:',
+ 'Email:' => '이메일:',
+ 'Notifications:' => '알림:',
+ 'Notifications' => '알림',
+ 'Account type:' => '계정종류:',
+ 'Edit profile' => '프로필 변경',
+ 'Change password' => '패스워드 변경',
+ 'Password modification' => '패스워드 변경',
+ 'External authentications' => '외부 인증',
+ 'Never connected.' => '접속기록없음',
+ 'No external authentication enabled.' => '외부 인증이 설정되어 있지 않습니다.',
+ 'Password modified successfully.' => '패스워드를 변경했습니다.',
+ 'Unable to change the password.' => '비밀 번호가 변경할 수 없었습니다.',
+ 'Change category for the task "%s"' => '할일 "%s"의 카테고리의 변경',
+ 'Change category' => '카테고리 수정',
+ '%s updated the task %s' => '%s이 할일 %s을 업데이트했습니다',
+ // '%s opened the task %s' => '',
+ '%s moved the task %s to the position #%d in the column "%s"' => '%s이 할일%s을 위치#%d컬럼%s로 옮겼습니다',
+ '%s moved the task %s to the column "%s"' => '%s이 할일 %s을 칼럼 "%s" 로 옮겼습니다',
+ '%s created the task %s' => '%s이 할일%s을 추가했습니다',
+ '%s closed the task %s' => '%s이 할일%s을 마쳤습니다',
+ '%s created a subtask for the task %s' => '%s이 할일%s의 서브 할일을 추가했습니다',
+ '%s updated a subtask for the task %s' => '%s이 할일%s의 서브 할일을 갱신했습니다',
+ 'Assigned to %s with an estimate of %s/%sh' => '담당자 %s에게 예상 %s/%sh로 할당되었습니다',
+ // 'Not assigned, estimate of %sh' => '',
+ '%s updated a comment on the task %s' => '%s이 할일%s의 댓글을 수정했습니다',
+ '%s commented the task %s' => '%s이 할일%s에 댓글을 남겼습니다',
+ '%s\'s activity' => '%s의 활동',
+ 'RSS feed' => 'RSS피드',
+ // '%s updated a comment on the task #%d' => '',
+ // '%s commented on the task #%d' => '',
+ // '%s updated a subtask for the task #%d' => '',
+ // '%s created a subtask for the task #%d' => '',
+ '%s updated the task #%d' => '%s이 할일#%d을 갱신했습니다',
+ '%s created the task #%d' => '%s이 할일#%d을 추가했습니다',
+ '%s closed the task #%d' => '%s이 할일#%d을 닫혔습니다',
+ '%s open the task #%d' => '%s이 할일#%d를 오픈했습니다',
+ '%s moved the task #%d to the column "%s"' => '%s이 할일#%d을 칼럼"%s"로 옮겼습니다',
+ // '%s moved the task #%d to the position %d in the column "%s"' => '',
+ 'Activity' => '활동',
+ // 'Default values are "%s"' => '',
+ // 'Default columns for new projects (Comma-separated)' => '',
+ 'Task assignee change' => '담당자의 변경',
+ // '%s change the assignee of the task #%d to %s' => '',
+ '%s changed the assignee of the task %s to %s' => '%s이 할일 %s의 담당을 %s로 변경했습니다',
+ 'New password for the user "%s"' => '사용자 "%s"의 새로운 패스워드',
+ 'Choose an event' => '행사의 선택',
+ 'Create a task from an external provider' => '할일을 외부 서비스로부터 작성하는 ',
+ 'Change the assignee based on an external username' => '담당자를 외부 서비스에 바탕을 두고 변경하는 ',
+ 'Change the category based on an external label' => '카테고리를 외부 서비스에 바탕을 두고 변경하는 ',
+ 'Reference' => '참조',
+ 'Label' => '라벨',
+ 'Database' => '데이터베이스',
+ 'About' => '정보',
+ 'Database driver:' => '데이터베이스 드라이버:',
+ 'Board settings' => '기본 설정',
+ 'URL and token' => 'URL와 토큰',
+ 'Webhook settings' => 'Webhook의 설정',
+ 'URL for task creation:' => 'Task작성의 URL:',
+ 'Reset token' => '토큰 리셋',
+ 'API endpoint:' => 'API엔드 포인트:',
+ 'Refresh interval for private board' => '비공개 보드의 갱신 빈도',
+ 'Refresh interval for public board' => '공개 보드의 갱신 빈도',
+ 'Task highlight period' => '할일의 하이라이트 기간',
+ // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '',
+ // 'Frequency in second (60 seconds by default)' => '',
+ // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '',
+ 'Application URL' => '애플리케이션의 URL',
+ 'Token regenerated.' => '토큰이 다시 생성되었습니다.',
+ 'Date format' => '데이터 포맷',
+ // 'ISO format is always accepted, example: "%s" and "%s"' => '',
+ 'New private project' => '새 비공개 프로젝트',
+ 'This project is private' => '이 프로젝트는 비공개입니다',
+ 'Add' => '추가',
+ 'Start date' => '시작시간',
+ 'Time estimated' => '예상시간',
+ 'There is nothing assigned to you.' => '할일이 없습니다. 옆사람의 일을 도와주면 어떨까요?',
+ 'My tasks' => '내 할일',
+ 'Activity stream' => '활동기록',
+ 'Dashboard' => '대시보드',
+ 'Confirmation' => '확인',
+ 'Allow everybody to access to this project' => '모든 사람이 이 프로젝트에 접근할 수 있도록 합니다',
+ 'Everybody have access to this project.' => '누구나 이 프로젝트에 액세스 할 수 있습니다',
+ 'Webhooks' => 'Webhook',
+ 'API' => 'API',
+ 'Create a comment from an external provider' => '외부 서비스로부터 의견을 작성한다',
+ 'Project management' => '프로젝트 관리',
+ 'My projects' => '내 프로젝트',
+ 'Columns' => '칼럼',
+ 'Task' => '할일',
+ 'Your are not member of any project.' => '어떤 프로젝트에도 속하지 않습니다.',
+ 'Percentage' => '비중',
+ 'Number of tasks' => '할일 수',
+ 'Task distribution' => '할일 분포',
+ 'Reportings' => '리포트',
+ // 'Task repartition for "%s"' => '',
+ 'Analytics' => '분석',
+ 'Subtask' => '서브 할일',
+ 'My subtasks' => '내 서브 할일',
+ 'User repartition' => '담당자 분포',
+ // 'User repartition for "%s"' => '',
+ 'Clone this project' => '이 프로젝트를 복제하는 ',
+ 'Column removed successfully.' => '(※)컬럼을 삭제했습니다',
+ 'Not enough data to show the graph.' => '그래프를 선묘화하려면 나왔지만 부족합니다',
+ 'Previous' => ' 돌아가',
+ 'The id must be an integer' => 'id은 숫자가 아니면 안 됩니다',
+ 'The project id must be an integer' => 'project id은 숫자가 아니면 안 됩니다',
+ 'The status must be an integer' => 'status는 숫자지 않으면 안 됩니다',
+ 'The subtask id is required' => 'subtask id가 필요합니다',
+ 'The subtask id must be an integer' => 'subtask id은 숫자가 아니면 안 됩니다',
+ 'The task id is required' => 'task id가 필요합니다',
+ 'The task id must be an integer' => 'task id은 숫자가 아니면 안 됩니다',
+ 'The user id must be an integer' => 'user id은 숫자가 아니면 안 됩니다',
+ 'This value is required' => '이 값이 필요합니다',
+ 'This value must be numeric' => '이 값은 숫자가 아니면 안 됩니다',
+ 'Unable to create this task.' => '이 할일을 작성할 수 없었습니다',
+ 'Cumulative flow diagram' => '축적 플로',
+ // 'Cumulative flow diagram for "%s"' => '',
+ 'Daily project summary' => '일시 프로젝트 개요',
+ 'Daily project summary export' => '일시 프로젝트 개요의 출력',
+ // 'Daily project summary export for "%s"' => '',
+ 'Exports' => '출력',
+ 'This export contains the number of tasks per column grouped per day.' => '이 출력은 날짜의 칼람별 할일 수를 집계한 것입니다',
+ 'Active swimlanes' => '액티브한 스윔레인',
+ 'Add a new swimlane' => ' 새로운 스윔레인',
+ 'Change default swimlane' => '기본 스윔레인의 변경',
+ 'Default swimlane' => '기본 스윔레인',
+ // 'Do you really want to remove this swimlane: "%s"?' => '',
+ 'Inactive swimlanes' => '인터랙티브한 스윔레인',
+ 'Remove a swimlane' => '스윔레인의 삭제',
+ 'Show default swimlane' => '기본 스윔레인의 표시',
+ // 'Swimlane modification for the project "%s"' => '',
+ 'Swimlane not found.' => '스윔레인이 발견되지 않습니다.',
+ 'Swimlane removed successfully.' => '스윔레인을 삭제했습니다.',
+ 'Swimlanes' => '스윔레인',
+ 'Swimlane updated successfully.' => '스윔레인을 갱신했습니다.',
+ 'The default swimlane have been updated successfully.' => '기본 스윔레인을 갱신했습니다.',
+ 'Unable to remove this swimlane.' => '스윔레인을 삭제할 수 없었습니다.',
+ 'Unable to update this swimlane.' => '스윔레인을 갱신할 수 없었습니다.',
+ 'Your swimlane have been created successfully.' => '스윔레인이 작성되었습니다.',
+ // 'Example: "Bug, Feature Request, Improvement"' => '',
+ // 'Default categories for new projects (Comma-separated)' => '',
+ 'Integrations' => '연계',
+ 'Integration with third-party services' => '외부 서비스 연계',
+ 'Subtask Id' => '서브 할일 Id',
+ 'Subtasks' => '서브 할일',
+ 'Subtasks Export' => '서브 할일 출력',
+ // 'Subtasks exportation for "%s"' => '',
+ 'Task Title' => '할일 제목',
+ 'Untitled' => '제목 없음',
+ 'Application default' => '애플리케이션 기본',
+ 'Language:' => '언어:',
+ 'Timezone:' => '시간대:',
+ 'All columns' => '모든 칼럼',
+ 'Calendar' => '달력',
+ 'Next' => '다음에 ',
+ '#%d' => '#%d',
+ 'All swimlanes' => '모든 스윔레인',
+ 'All colors' => '모든 색',
+ // 'Moved to column %s' => '',
+ 'Change description' => '설명 수정',
+ 'User dashboard' => '대시보드',
+ 'Allow only one subtask in progress at the same time for a user' => '한 사용자에 대한 하나의 할일만 진행 중에 가능합니다',
+ // 'Edit column "%s"' => '',
+ // 'Select the new status of the subtask: "%s"' => '',
+ 'Subtask timesheet' => '서브 할일 타임시트',
+ 'There is nothing to show.' => '기록이 없습니다',
+ 'Time Tracking' => '타임 트레킹',
+ 'You already have one subtask in progress' => '이미 진행 중인 서브 할일가 있습니다.',
+ 'Which parts of the project do you want to duplicate?' => '프로젝트의 무엇을 복제합니까?',
+ // 'Disallow login form' => '',
+ 'Start' => '시작',
+ 'End' => '종료',
+ 'Task age in days' => '할일이 생긴 시간',
+ 'Days in this column' => '이 칼럼에 있는 시간',
+ '%dd' => '%d일',
+ 'Add a new link' => ' 새로운 링크 추가',
+ // 'Do you really want to remove this link: "%s"?' => '',
+ // 'Do you really want to remove this link with task #%d?' => '',
+ 'Field required' => '필드가 필요합니다',
+ 'Link added successfully.' => '링크를 추가했습니다.',
+ 'Link updated successfully.' => '링크를 갱신했습니다.',
+ 'Link removed successfully.' => '링크를 삭제했습니다.',
+ 'Link labels' => '링크 라벨',
+ 'Link modification' => '링크의 변경',
+ 'Links' => '링크',
+ 'Link settings' => '링크 설정',
+ 'Opposite label' => '반대의 라벨',
+ 'Remove a link' => '라벨의 삭제',
+ 'Task\'s links' => '할일의 라벨',
+ 'The labels must be different' => ' 다른 라벨을 지정하세요',
+ 'There is no link.' => '링크가 없습니다',
+ 'This label must be unique' => '라벨은 독특할 필요가 있습니다',
+ 'Unable to create your link.' => '링크를 작성할 수 없었습니다.',
+ 'Unable to update your link.' => '링크를 갱신할 수 없었습니다.',
+ 'Unable to remove this link.' => '링크를 삭제할 수 없었습니다.',
+ 'relates to' => '연관 링크',
+ 'blocks' => '다음을 딜레이하는',
+ 'is blocked by' => '다음 때문에 딜레이되는',
+ 'duplicates' => '다음과 중복하는',
+ 'is duplicated by' => '다음에 중복되는',
+ 'is a child of' => '다음의 하위 할일',
+ 'is a parent of' => '다음의 상위 할일',
+ 'targets milestone' => '다음의 이정표를 목표로 하는',
+ 'is a milestone of' => '다음의 이정표인',
+ 'fixes' => '다음을 수정하는',
+ 'is fixed by' => '다음에 의해 수정되는',
+ 'This task' => '이 할일의 ',
+ '<1h' => '<1시간',
+ '%dh' => '%d시간',
+ 'Expand tasks' => '할일 크게',
+ 'Collapse tasks' => '할일 작게',
+ 'Expand/collapse tasks' => '할일 크게/작게',
+ 'Close dialog box' => '다이얼로그를 닫습니다',
+ 'Submit a form' => '제출',
+ 'Board view' => '보드 뷰',
+ 'Keyboard shortcuts' => '키보드 숏 컷',
+ 'Open board switcher' => '보드 전환을 열',
+ 'Application' => '애플리케이션',
+ 'Compact view' => '컴팩트 뷰',
+ 'Horizontal scrolling' => '세로 스크롤',
+ 'Compact/wide view' => '컴팩트/와이드 뷰',
+ 'No results match:' => '결과가 일치하지 않았습니다',
+ 'Currency' => '통화',
+ 'Private project' => '개인 프로젝트',
+ // 'AUD - Australian Dollar' => '',
+ // 'CAD - Canadian Dollar' => '',
+ // 'CHF - Swiss Francs' => '',
+ 'Custom Stylesheet' => '커스텀 스타일 시트',
+ 'download' => '다운로드',
+ // 'EUR - Euro' => '',
+ // 'GBP - British Pound' => '',
+ // 'INR - Indian Rupee' => '',
+ // 'JPY - Japanese Yen' => '',
+ // 'NZD - New Zealand Dollar' => '',
+ // 'RSD - Serbian dinar' => '',
+ // 'USD - US Dollar' => '',
+ 'Destination column' => '이동 후 칼럼',
+ 'Move the task to another column when assigned to a user' => '사용자의 할당을 하면 할일을 다른 칼럼에 이동',
+ 'Move the task to another column when assignee is cleared' => '사용자의 할당이 없어지면 할일을 다른 칼럼에 이동',
+ 'Source column' => '이동 전 칼럼',
+ 'Transitions' => '이력',
+ 'Executer' => '실행자',
+ 'Time spent in the column' => '칼럼에 있던 시간',
+ 'Task transitions' => '할일 천이',
+ 'Task transitions export' => '할일 천이를 출력',
+ 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '이 리포트는 할일의 칼럼 간 이동을 시간, 유저, 경과 시간과 함께 기록한 것입니다.',
+ 'Currency rates' => '환율',
+ 'Rate' => '레이트',
+ 'Change reference currency' => '현재의 기축 통화',
+ 'Add a new currency rate' => ' 새로운 통화 환율을 추가',
+ 'Reference currency' => '기축 통화',
+ // 'The currency rate have been added successfully.' => '',
+ 'Unable to add this currency rate.' => '이 통화 환율을 추가할 수 없습니다.',
+ 'Webhook URL' => 'Webhook URL',
+ '%s remove the assignee of the task %s' => '%s이 할일 %s의 담당을 삭제했습니다',
+ 'Enable Gravatar images' => 'Gravatar이미지를 활성화',
+ 'Information' => '정보',
+ 'Check two factor authentication code' => '2단 인증을 체크한다',
+ 'The two factor authentication code is not valid.' => '2단 인증 코드는 무효입니다.',
+ 'The two factor authentication code is valid.' => '2단 인증 코드는 유효합니다.',
+ 'Code' => '코드',
+ 'Two factor authentication' => '2단 인증',
+ // 'This QR code contains the key URI: ' => '',
+ 'Check my code' => '코드 체크',
+ // 'Secret key: ' => '',
+ 'Test your device' => '디바이스 테스트',
+ // 'Assign a color when the task is moved to a specific column' => '',
+ '%s via Kanboard' => '%s via E-board',
+ // 'Burndown chart for "%s"' => '',
+ // 'Burndown chart' => '',
+ // 'This chart show the task complexity over the time (Work Remaining).' => '',
+ 'Screenshot taken %s' => '스크린샷_%s',
+ 'Add a screenshot' => '스크린샷 추가',
+ 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '스크린샷을 CTRL+V 혹은 ⌘+V를 눌러 붙여넣기',
+ 'Screenshot uploaded successfully.' => '스크린샷을 업로드하였습니다',
+ // 'SEK - Swedish Krona' => '',
+ // 'Identifier' => '',
+ // 'Disable two factor authentication' => '',
+ // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
+ // 'Edit link' => '',
+ 'Start to type task title...' => '할일 제목을 처음부터 입력해주세요',
+ // 'A task cannot be linked to itself' => '',
+ // 'The exact same link already exists' => '',
+ // 'Recurrent task is scheduled to be generated' => '',
+ // 'Score' => '',
+ // 'The identifier must be unique' => '',
+ // 'This linked task id doesn\'t exists' => '',
+ // 'This value must be alphanumeric' => '',
+ 'Edit recurrence' => '반복 수정',
+ 'Generate recurrent task' => '반복 할일 만들기',
+ 'Trigger to generate recurrent task' => '반복 할일 트리거 만들기',
+ 'Factor to calculate new due date' => '새로운 종료날짜 계산',
+ 'Timeframe to calculate new due date' => '종료날짜 계산 단위',
+ 'Base date to calculate new due date' => '새로운 기본 종료날짜 계산',
+ 'Action date' => '시작날짜',
+ 'Base date to calculate new due date: ' => '새로운 기본 종료날짜 계산: ',
+ // 'This task has created this child task: ' => '',
+ 'Day(s)' => '일',
+ 'Existing due date' => '기존 종료날짜',
+ 'Factor to calculate new due date: ' => '새로운 종료날짜 계산: ',
+ 'Month(s)' => '월',
+ // 'Recurrence' => '',
+ // 'This task has been created by: ' => '',
+ // 'Recurrent task has been generated:' => '',
+ 'Timeframe to calculate new due date: ' => '종료날짜 계산 단위',
+ // 'Trigger to generate recurrent task: ' => '',
+ 'When task is closed' => '할일을 마쳤을때',
+ 'When task is moved from first column' => '할일이 첫번째 칼럼으로 옮겨졌을때',
+ 'When task is moved to last column' => '할일이 마지막 칼럼으로 옮겨졌을때',
+ 'Year(s)' => '년',
+ // 'Calendar settings' => '',
+ // 'Project calendar view' => '',
+ 'Project settings' => '프로젝트 설정',
+ // 'Show subtasks based on the time tracking' => '',
+ // 'Show tasks based on the creation date' => '',
+ // 'Show tasks based on the start date' => '',
+ // 'Subtasks time tracking' => '',
+ // 'User calendar view' => '',
+ // 'Automatically update the start date' => '',
+ // 'iCal feed' => '',
+ // 'Preferences' => '',
+ // 'Security' => '',
+ 'Two factor authentication disabled' => '2단 인증 비활성화',
+ // 'Two factor authentication enabled' => '',
+ // 'Unable to update this user.' => '',
+ // 'There is no user management for private projects.' => '',
+ // 'User that will receive the email' => '',
+ 'Email subject' => '이메일 제목',
+ 'Date' => '날짜',
+ // 'Add a comment log when moving the task between columns' => '',
+ // 'Move the task to another column when the category is changed' => '',
+ 'Send a task by email to someone' => '할일을 이메일로 보내기',
+ 'Reopen a task' => '할일 다시 시작',
+ 'Column change' => '칼럼 이동',
+ 'Position change' => '위치 이동',
+ 'Swimlane change' => '스윔레인 변경',
+ 'Assignee change' => '담당자 변경',
+ '[%s] Overdue tasks' => '[%s] 마감시간 지남',
+ 'Notification' => '알림',
+ // '%s moved the task #%d to the first swimlane' => '',
+ // '%s moved the task #%d to the swimlane "%s"' => '',
+ 'Swimlane' => '스윔레인',
+ // 'Gravatar' => '',
+ // '%s moved the task %s to the first swimlane' => '',
+ // '%s moved the task %s to the swimlane "%s"' => '',
+ // 'This report contains all subtasks information for the given date range.' => '',
+ // 'This report contains all tasks information for the given date range.' => '',
+ // 'Project activities for %s' => '',
+ // 'view the board on Kanboard' => '',
+ // 'The task have been moved to the first swimlane' => '',
+ // 'The task have been moved to another swimlane:' => '',
+ // 'Overdue tasks for the project "%s"' => '',
+ 'New title: %s' => '제목 변경: %s',
+ 'The task is not assigned anymore' => '담당자 없음',
+ 'New assignee: %s' => '담당자 변경: %s',
+ 'There is no category now' => '카테고리 없음',
+ 'New category: %s' => '카테고리 변경: %s',
+ 'New color: %s' => '색깔 변경: %s',
+ 'New complexity: %d' => '복잡도 변경: %d',
+ 'The due date have been removed' => '마감날짜 삭제',
+ 'There is no description anymore' => '설명 없음',
+ 'Recurrence settings have been modified' => '반복할일 설정 수정',
+ 'Time spent changed: %sh' => '경과시간 변경: %s시간',
+ 'Time estimated changed: %sh' => '%s시간으로 예상시간 변경',
+ // 'The field "%s" have been updated' => '',
+ // 'The description has been modified:' => '',
+ 'Do you really want to close the task "%s" as well as all subtasks?' => '할일 "%s"과 서브 할일을 모두 마치시겠습니까?',
+ 'I want to receive notifications for:' => '다음의 알림을 받기를 원합니다:',
+ 'All tasks' => '모든 할일',
+ 'Only for tasks assigned to me' => '내가 담당자인 일',
+ 'Only for tasks created by me' => '내가 만든 일',
+ 'Only for tasks created by me and assigned to me' => '내가 만들었거나 내가 담당자인 일',
+ // '%%Y-%%m-%%d' => '',
+ // 'Total for all columns' => '',
+ // 'You need at least 2 days of data to show the chart.' => '',
+ '<15m' => '<15분',
+ '<30m' => '<30분',
+ // 'Stop timer' => '',
+ 'Start timer' => '타이머 시작',
+ // 'Add project member' => '',
+ 'Enable notifications' => '알림 켜기',
+ 'My activity stream' => '내 활동기록',
+ 'My calendar' => '내 캘린더',
+ // 'Search tasks' => '',
+ 'Reset filters' => '필터 리셋',
+ 'My tasks due tomorrow' => '내일까지 내 할일',
+ 'Tasks due today' => '오늘까지 할일',
+ 'Tasks due tomorrow' => '내일까지 할일',
+ 'Tasks due yesterday' => '어제까지 할일',
+ 'Closed tasks' => '닫힌 할일',
+ 'Open tasks' => '열린 할일',
+ 'Not assigned' => '담당자가 없는 일',
+ 'View advanced search syntax' => '추가 검색 문법보기',
+ 'Overview' => '개요',
+ // 'Board/Calendar/List view' => '',
+ // 'Switch to the board view' => '',
+ // 'Switch to the calendar view' => '',
+ // 'Switch to the list view' => '',
+ // 'Go to the search/filter box' => '',
+ 'There is no activity yet.' => '활동이 없습니다',
+ // 'No tasks found.' => '',
+ // 'Keyboard shortcut: "%s"' => '',
+ 'List' => '목록',
+ // 'Filter' => '',
+ 'Advanced search' => '검색 문법',
+ 'Example of query: ' => '문법 예제 ',
+ 'Search by project: ' => '프로젝트로 찾기 ',
+ 'Search by column: ' => '칼럼으로 찾기 ',
+ 'Search by assignee: ' => '담당자로 찾기 ',
+ 'Search by color: ' => '색깔로 찾기 ',
+ 'Search by category: ' => '카테고리로 찾기 ',
+ 'Search by description: ' => '설명으로 찾기 ',
+ 'Search by due date: ' => '마감날짜로 찾기 ',
+ // 'Lead and Cycle time for "%s"' => '',
+ // 'Average time spent into each column for "%s"' => '',
+ // 'Average time spent into each column' => '',
+ // 'Average time spent' => '',
+ // 'This chart show the average time spent into each column for the last %d tasks.' => '',
+ // 'Average Lead and Cycle time' => '',
+ // 'Average lead time: ' => '',
+ // 'Average cycle time: ' => '',
+ 'Cycle Time' => '사이클 타임',
+ 'Lead Time' => '리드 타임',
+ // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '',
+ // 'Average time into each column' => '',
+ // 'Lead and cycle time' => '',
+ 'Lead time: ' => '리드 타임: ',
+ 'Cycle time: ' => '사이클 타임: ',
+ 'Time spent into each column' => '각 칼럼에서 걸린 시간',
+ // 'The lead time is the duration between the task creation and the completion.' => '',
+ // 'The cycle time is the duration between the start date and the completion.' => '',
+ // 'If the task is not closed the current time is used instead of the completion date.' => '',
+ // 'Set automatically the start date' => '',
+ 'Edit Authentication' => '계정 수정',
+ // 'Remote user' => '',
+ // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '',
+ // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '',
+ 'New remote user' => '새로운 원격유저',
+ 'New local user' => '새로운 유저',
+ // 'Default task color' => '',
+ 'This feature does not work with all browsers.' => '이 기능은 일부 브라우저에서 작동하지 않습니다',
+ // 'There is no destination project available.' => '',
+ // 'Trigger automatically subtask time tracking' => '',
+ // 'Include closed tasks in the cumulative flow diagram' => '',
+ // 'Current swimlane: %s' => '',
+ // 'Current column: %s' => '',
+ // 'Current category: %s' => '',
+ // 'no category' => '',
+ // 'Current assignee: %s' => '',
+ // 'not assigned' => '',
+ // 'Author:' => '',
+ // 'contributors' => '',
+ // 'License:' => '',
+ // 'License' => '',
+ // 'Enter the text below' => '',
+ // 'Gantt chart for %s' => '',
+ // 'Sort by position' => '',
+ // 'Sort by date' => '',
+ // 'Add task' => '',
+ // 'Start date:' => '',
+ // 'Due date:' => '',
+ // 'There is no start date or due date for this task.' => '',
+ // 'Moving or resizing a task will change the start and due date of the task.' => '',
+ // 'There is no task in your project.' => '',
+ 'Gantt chart' => '간트차트',
+ 'People who are project managers' => '프로젝트 매니저',
+ 'People who are project members' => '프로젝트 멤버',
+ // 'NOK - Norwegian Krone' => '',
+ 'Show this column' => '칼럼 보이기',
+ 'Hide this column' => '칼럼 숨기기',
+ 'open file' => '열기',
+ 'End date' => '종료 날짜',
+ 'Users overview' => '유저 전체보기',
+ 'Members' => '멤버',
+ // 'Shared project' => '',
+ 'Project managers' => '프로젝트 매니저',
+ // 'Gantt chart for all projects' => '',
+ 'Projects list' => '프로젝트 리스트',
+ 'Gantt chart for this project' => '이 프로젝트 간트차트',
+ 'Project board' => '프로젝트 보드',
+ // 'End date:' => '',
+ 'There is no start date or end date for this project.' => '이 프로젝트에는 시작날짜와 종료날짜가 없습니다',
+ 'Projects Gantt chart' => '프로젝트 간트차트',
+ // 'Change task color when using a specific task link' => '',
+ // 'Task link creation or modification' => '',
+ // 'Milestone' => '',
+ // 'Documentation: %s' => '',
+ // 'Switch to the Gantt chart view' => '',
+ // 'Reset the search/filter box' => '',
+ // 'Documentation' => '',
+ // 'Table of contents' => '',
+ 'Gantt' => '간트',
+ // 'Author' => '',
+ // 'Version' => '',
+ // 'Plugins' => '',
+ // 'There is no plugin loaded.' => '',
+ 'Set maximum column height' => '최대 칼럼 높이 제한하기',
+ 'Remove maximum column height' => '최대 칼럼 높이 없애기',
+ 'My notifications' => '내 알림',
+ 'Custom filters' => '사용자 정의 필터',
+ // 'Your custom filter have been created successfully.' => '',
+ // 'Unable to create your custom filter.' => '',
+ // 'Custom filter removed successfully.' => '',
+ // 'Unable to remove this custom filter.' => '',
+ // 'Edit custom filter' => '',
+ // 'Your custom filter have been updated successfully.' => '',
+ // 'Unable to update custom filter.' => '',
+ 'Web' => '웹',
+ // 'New attachment on task #%d: %s' => '',
+ 'New comment on task #%d' => '할일 #%d에 새로운 댓글이 달렸습니다',
+ 'Comment updated on task #%d' => '할일 #%d에 댓글이 업데이트되었습니다',
+ 'New subtask on task #%d' => '서브 할일 #%d이 업데이트되었습니다',
+ 'Subtask updated on task #%d' => '서브 할일 #%d가 업데이트되었습니다',
+ 'New task #%d: %s' => '할일 #%d: %s이 추가되었습니다',
+ 'Task updated #%d' => '할일 #%d이 업데이트되었습니다',
+ 'Task #%d closed' => '할일 #%d를 마쳤습니다',
+ // 'Task #%d opened' => '',
+ 'Column changed for task #%d' => '할일 #%d의 칼럼이 변경되었습니다',
+ 'New position for task #%d' => '할일 #%d이 새로운 위치에 등록되었습니다',
+ // 'Swimlane changed for task #%d' => '',
+ // 'Assignee changed on task #%d' => '',
+ // '%d overdue tasks' => '',
+ // 'Task #%d is overdue' => '',
+ 'No new notifications.' => '알림이 없습니다',
+ 'Mark all as read' => '모두 읽음',
+ 'Mark as read' => '읽음',
+ // 'Total number of tasks in this column across all swimlanes' => '',
+ // 'Collapse swimlane' => '',
+ // 'Expand swimlane' => '',
+ // 'Add a new filter' => '',
+ // 'Share with all project members' => '',
+ // 'Shared' => '',
+ // 'Owner' => '',
+ 'Unread notifications' => '읽지않은 알림',
+ 'Notification methods:' => '알림 방법',
+ // 'Import tasks from CSV file' => '',
+ // 'Unable to read your file' => '',
+ // '%d task(s) have been imported successfully.' => '',
+ // 'Nothing have been imported!' => '',
+ // 'Import users from CSV file' => '',
+ // '%d user(s) have been imported successfully.' => '',
+ // 'Comma' => '',
+ // 'Semi-colon' => '',
+ // 'Tab' => '',
+ // 'Vertical bar' => '',
+ // 'Double Quote' => '',
+ // 'Single Quote' => '',
+ // '%s attached a file to the task #%d' => '',
+ // 'There is no column or swimlane activated in your project!' => '',
+ // 'Append filter (instead of replacement)' => '',
+ // 'Append/Replace' => '',
+ // 'Append' => '',
+ // 'Replace' => '',
+ 'Import' => '가져오기',
+ // 'change sorting' => '',
+ // 'Tasks Importation' => '',
+ // 'Delimiter' => '',
+ // 'Enclosure' => '',
+ // 'CSV File' => '',
+ // 'Instructions' => '',
+ // 'Your file must use the predefined CSV format' => '',
+ // 'Your file must be encoded in UTF-8' => '',
+ // 'The first row must be the header' => '',
+ // 'Duplicates are not verified for you' => '',
+ // 'The due date must use the ISO format: YYYY-MM-DD' => '',
+ // 'Download CSV template' => '',
+ 'No external integration registered.' => '설정이 되어있지 않습니다',
+ // 'Duplicates are not imported' => '',
+ // 'Usernames must be lowercase and unique' => '',
+ // 'Passwords will be encrypted if present' => '',
+ '%s attached a new file to the task %s' => '%s이 새로운 파일을 할일 %s에 추가했습니다',
+ // 'Link type' => '',
+ // 'Assign automatically a category based on a link' => '',
+ // 'BAM - Konvertible Mark' => '',
+ // 'Assignee Username' => '',
+ // 'Assignee Name' => '',
+ // 'Groups' => '',
+ // 'Members of %s' => '',
+ // 'New group' => '',
+ // 'Group created successfully.' => '',
+ // 'Unable to create your group.' => '',
+ // 'Edit group' => '',
+ // 'Group updated successfully.' => '',
+ // 'Unable to update your group.' => '',
+ // 'Add group member to "%s"' => '',
+ // 'Group member added successfully.' => '',
+ // 'Unable to add group member.' => '',
+ // 'Remove user from group "%s"' => '',
+ // 'User removed successfully from this group.' => '',
+ // 'Unable to remove this user from the group.' => '',
+ // 'Remove group' => '',
+ // 'Group removed successfully.' => '',
+ // 'Unable to remove this group.' => '',
+ // 'Project Permissions' => '',
+ 'Manager' => '매니저',
+ 'Project Manager' => '프로젝트 매니저',
+ 'Project Member' => '프로젝트 멤버',
+ // 'Project Viewer' => '',
+ // 'Your account is locked for %d minutes' => '',
+ // 'Invalid captcha' => '',
+ // 'The name must be unique' => '',
+ 'View all groups' => '모든그룹보기',
+ // 'View group members' => '',
+ // 'There is no user available.' => '',
+ // 'Do you really want to remove the user "%s" from the group "%s"?' => '',
+ // 'There is no group.' => '',
+ // 'External Id' => '',
+ 'Add group member' => '멤버추가',
+ // 'Do you really want to remove this group: "%s"?' => '',
+ // 'There is no user in this group.' => '',
+ // 'Remove this user' => '',
+ 'Permissions' => '권한',
+ // 'Allowed Users' => '',
+ // 'No user have been allowed specifically.' => '',
+ 'Role' => '역할',
+ // 'Enter user name...' => '',
+ // 'Allowed Groups' => '',
+ // 'No group have been allowed specifically.' => '',
+ // 'Group' => '',
+ // 'Group Name' => '',
+ // 'Enter group name...' => '',
+ 'Role:' => '역할: ',
+ 'Project members' => '프로젝트 멤버',
+ // 'Compare hours for "%s"' => '',
+ // '%s mentioned you in the task #%d' => '',
+ // '%s mentioned you in a comment on the task #%d' => '',
+ // 'You were mentioned in the task #%d' => '',
+ 'You were mentioned in a comment on the task #%d' => '할일 #%d의 댓글에서 언급되었습니다',
+ // 'Mentioned' => '',
+ // 'Compare Estimated Time vs Actual Time' => '',
+ // 'Estimated hours: ' => '',
+ // 'Actual hours: ' => '',
+ // 'Hours Spent' => '',
+ // 'Hours Estimated' => '',
+ // 'Estimated Time' => '',
+ // 'Actual Time' => '',
+ // 'Estimated vs actual time' => '',
+ // 'RUB - Russian Ruble' => '',
+ // 'Assign the task to the person who does the action when the column is changed' => '',
+ // 'Close a task in a specific column' => '',
+ 'Time-based One-time Password Algorithm' => '시간에 기반한 1회용 패스워드 알고리즘',
+ 'Two-Factor Provider: ' => '2단 인증: ',
+ // 'Disable two-factor authentication' => '',
+ 'Enable two-factor authentication' => '2단 인증 활성화',
+ // 'There is no integration registered at the moment.' => '',
+ // 'Password Reset for Kanboard' => '',
+ 'Forgot password?' => '비밀번호 찾기',
+ // 'Enable "Forget Password"' => '',
+ // 'Password Reset' => '',
+ // 'New password' => '',
+ // 'Change Password' => '',
+ // 'To reset your password click on this link:' => '',
+ 'Last Password Reset' => '비밀번호 초기화',
+ 'The password has never been reinitialized.' => '비밀번호가 초기화되지 않았습니다',
+ // 'Creation' => '',
+ // 'Expiration' => '',
+ 'Password reset history' => '비밀번호 초기화 기록',
+ // 'All tasks of the column "%s" and the swimlane "%s" have been closed successfully.' => '',
+ // 'Do you really want to close all tasks of this column?' => '',
+ // '%d task(s) in the column "%s" and the swimlane "%s" will be closed.' => '',
+ 'Close all tasks of this column' => '칼럼의 모든 할일 마치기',
+ // 'No plugin has registered a project notification method. You can still configure individual notifications in your user profile.' => '',
+ 'My dashboard' => '대시보드',
+ 'My profile' => '프로필',
+ // 'Project owner: ' => '',
+ // 'The project identifier is optional and must be alphanumeric, example: MYPROJECT.' => '',
+ // 'Project owner' => '',
+ // 'Those dates are useful for the project Gantt chart.' => '',
+ // 'Private projects do not have users and groups management.' => '',
+ // 'There is no project member.' => '',
+ // 'Priority' => '',
+ // 'Task priority' => '',
+ // 'General' => '',
+ // 'Dates' => '',
+ // 'Default priority' => '',
+ // 'Lowest priority' => '',
+ // 'Highest priority' => '',
+ // 'If you put zero to the low and high priority, this feature will be disabled.' => '',
+ // 'Close a task when there is no activity' => '',
+ // 'Duration in days' => '',
+ // 'Send email when there is no activity on a task' => '',
+ // 'Unable to fetch link information.' => '',
+ // 'Daily background job for tasks' => '',
+ // 'Auto' => '',
+ // 'Related' => '',
+ // 'Attachment' => '',
+ // 'Title not found' => '',
+ // 'Web Link' => '',
+ // 'External links' => '',
+ // 'Add external link' => '',
+ // 'Type' => '',
+ // 'Dependency' => '',
+ // 'Add internal link' => '',
+ // 'Add a new external link' => '',
+ // 'Edit external link' => '',
+ // 'External link' => '',
+ // 'Copy and paste your link here...' => '',
+ // 'URL' => '',
+ // 'Internal links' => '',
+ // 'Assign to me' => '',
+ // 'Me' => '',
+ // 'Do not duplicate anything' => '',
+ // 'Projects management' => '',
+ // 'Users management' => '',
+ // 'Groups management' => '',
+ // 'Create from another project' => '',
+ // 'open' => '',
+ // 'closed' => '',
+ // 'Priority:' => '',
+ // 'Reference:' => '',
+ // 'Complexity:' => '',
+ // 'Swimlane:' => '',
+ // 'Column:' => '',
+ // 'Position:' => '',
+ // 'Creator:' => '',
+ // 'Time estimated:' => '',
+ // '%s hours' => '',
+ // 'Time spent:' => '',
+ // 'Created:' => '',
+ // 'Modified:' => '',
+ // 'Completed:' => '',
+ // 'Started:' => '',
+ // 'Moved:' => '',
+ // 'Task #%d' => '',
+ // 'Date and time format' => '',
+ // 'Time format' => '',
+ // 'Start date: ' => '',
+ // 'End date: ' => '',
+ // 'New due date: ' => '',
+ // 'Start date changed: ' => '',
+ // 'Disable private projects' => '',
+ // 'Do you really want to remove this custom filter: "%s"?' => '',
+ // 'Remove a custom filter' => '',
+ // 'User activated successfully.' => '',
+ // 'Unable to enable this user.' => '',
+ // 'User disabled successfully.' => '',
+ // 'Unable to disable this user.' => '',
+ // 'All files have been uploaded successfully.' => '',
+ // 'View uploaded files' => '',
+ // 'The maximum allowed file size is %sB.' => '',
+ // 'Choose files again' => '',
+ // 'Drag and drop your files here' => '',
+ // 'choose files' => '',
+ // 'View profile' => '',
+ // 'Two Factor' => '',
+ // 'Disable user' => '',
+ // 'Do you really want to disable this user: "%s"?' => '',
+ // 'Enable user' => '',
+ // 'Do you really want to enable this user: "%s"?' => '',
+ // 'Download' => '',
+ // 'Uploaded: %s' => '',
+ // 'Size: %s' => '',
+ // 'Uploaded by %s' => '',
+ // 'Filename' => '',
+ // 'Size' => '',
+ // 'Column created successfully.' => '',
+ // 'Another column with the same name exists in the project' => '',
+ // 'Default filters' => '',
+ // 'Your board doesn\'t have any column!' => '',
+ // 'Change column position' => '',
+ // 'Switch to the project overview' => '',
+ // 'User filters' => '',
+ // 'Category filters' => '',
+ // 'Upload a file' => '',
+ // 'View file' => '',
+ // 'Last activity' => '',
+ // 'Change subtask position' => '',
+ // 'This value must be greater than %d' => '',
+ // 'Another swimlane with the same name exists in the project' => '',
+ // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '',
+ // 'Actions duplicated successfully.' => '',
+ // 'Unable to duplicate actions.' => '',
+ // 'Add a new action' => '',
+ // 'Import from another project' => '',
+ // 'There is no action at the moment.' => '',
+ // 'Import actions from another project' => '',
+ // 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
+);
diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php
index fc1447e9..4537f38c 100644
--- a/app/Locale/my_MY/translations.php
+++ b/app/Locale/my_MY/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Semua projek',
'Add a new column' => 'Tambah kolom baru',
'Title' => 'Judul',
- 'Nobody assigned' => 'Tidak ada yang ditugaskan',
'Assigned to %s' => 'Ditugaskan ke %s',
'Remove a column' => 'Hapus kolom',
'Remove a column from a board' => 'Hapus kolom dari papan',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Jumlah tugas',
'User' => 'Pengguna',
'Comments' => 'Komentar',
- 'Write your text in Markdown' => 'Menulis teks anda didalam Markdown',
'Leave a comment' => 'Tinggalkan komentar',
'Comment is required' => 'Komentar diperlukan',
'Leave a description' => 'Tinggalkan deskripsi',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Pelacakan waktu :',
'New sub-task' => 'Sub-tugas baru',
'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »',
- 'Comment updated' => 'Komentar diperbaharui',
'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »',
'New attachment' => 'Lampirkan baru',
'New comment' => 'Komentar baru',
+ 'Comment updated' => 'Komentar diperbaharui',
'New subtask' => 'Sub-tugas baru',
'Subtask updated' => 'Sub-tugas diperbaharui',
'Task updated' => 'Tugas diperbaharui',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO selalunya diterima, contoh: « %s » et « %s »',
'New private project' => 'Projek peribadi baharu',
'This project is private' => 'projek ini adalah peribadi',
- 'Type here to create a new sub-task' => 'Ketik disini untuk membuat sub-tugas baru',
'Add' => 'Tambah',
'Start date' => 'Tarikh mula',
'Time estimated' => 'Anggaran masa',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Ekspor ringkasan projek harian untuk « %s »',
'Exports' => 'Ekspor',
'This export contains the number of tasks per column grouped per day.' => 'Ekspor ini berisi jumlah dari tugas per kolom dikelompokan perhari.',
- 'Nothing to preview...' => 'Tiada yang dapat diintai...',
- 'Preview' => 'Intai',
- 'Write' => 'Tulis',
'Active swimlanes' => 'Swimlanes aktif',
'Add a new swimlane' => 'Tambah swimlane baharu',
'Change default swimlane' => 'Tukar piawai swimlane',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Usia tugas dalam bentuk harian',
'Days in this column' => 'Hari dalam kolom ini',
'%dd' => '%dj',
- 'Add a link' => 'Menambahkan pautan',
'Add a new link' => 'Tambah Pautan baru',
'Do you really want to remove this link: "%s"?' => 'Anda yakin akan menghapus Pautan ini : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Anda yakin akan menghapus Pautan ini dengan tugas n°%d ?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Aliran kegiatan saya',
'My calendar' => 'Kalender saya',
'Search tasks' => 'Cari tugas',
- 'Back to the calendar' => 'Kembali ke kalender',
- 'Filters' => 'Filter',
'Reset filters' => 'Reset ulang filter',
'My tasks due tomorrow' => 'Tugas saya yang berakhir besok',
'Tasks due today' => 'Tugas yang berakhir hari ini',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Waktu berakhir :',
'There is no start date or end date for this project.' => 'Tidak ada waktu mula atau waktu berakhir pada projek ini',
'Projects Gantt chart' => 'projekkan carta Gantt',
- 'Link type' => 'Jenis pautan',
'Change task color when using a specific task link' => 'Rubah warna tugas ketika menggunakan Pautan tugas yang spesifik',
'Task link creation or modification' => 'Pautan tugas pada penciptaan atau penyuntingan',
'Milestone' => 'Batu Tanda',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ 'Link type' => 'Jenis pautan',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php
index 003ca856..8c6a56f2 100644
--- a/app/Locale/nb_NO/translations.php
+++ b/app/Locale/nb_NO/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alle prosjekter',
'Add a new column' => 'Legg til en ny kolonne',
'Title' => 'Tittel',
- 'Nobody assigned' => 'Ikke tildelt',
'Assigned to %s' => 'Tildelt: %s',
'Remove a column' => 'Fjern en kolonne',
'Remove a column from a board' => 'Fjern en kolonne fra et board',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Antall oppgaver',
'User' => 'Bruker',
'Comments' => 'Kommentarer',
- 'Write your text in Markdown' => 'Skriv din tekst i markdown',
'Leave a comment' => 'Legg inn en kommentar',
'Comment is required' => 'Kommentar må legges inn',
'Leave a description' => 'Legg inn en beskrivelse...',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Tidsmåling:',
'New sub-task' => 'Ny deloppgave',
'New attachment added "%s"' => 'Nytt vedlegg er lagt tilet "%s"',
- 'Comment updated' => 'Kommentar oppdatert',
'New comment posted by %s' => 'Ny kommentar fra %s',
'New attachment' => 'Nytt vedlegg',
'New comment' => 'Ny kommentar',
+ 'Comment updated' => 'Kommentar oppdatert',
'New subtask' => 'Ny deloppgave',
'Subtask updated' => 'Deloppgave oppdatert',
'Task updated' => 'Oppgave oppdatert',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er alltid akseptert, eksempelvis: "%s" og "%s"',
'New private project' => 'Nytt privat prosjekt',
'This project is private' => 'Dette projektet er privat',
- 'Type here to create a new sub-task' => 'Skriv her for ø opprette en ny deloppgave',
'Add' => 'Legg til',
'Start date' => 'Start dato',
'Time estimated' => 'Tid estimert',
@@ -483,9 +480,6 @@ return array(
// 'Daily project summary export for "%s"' => '',
'Exports' => 'Eksporter',
// 'This export contains the number of tasks per column grouped per day.' => '',
- 'Nothing to preview...' => 'Ingenting å forhåndsvise',
- 'Preview' => 'Forhåndsvisning',
- 'Write' => 'Skriv',
'Active swimlanes' => 'Aktive svømmebaner',
'Add a new swimlane' => 'Legg til en ny svømmebane',
'Change default swimlane' => 'Endre standard svømmebane',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Dager siden oppgaven ble opprettet',
'Days in this column' => 'Dager siden oppgaven ble lagt i denne kolonnen',
// '%dd' => '',
- 'Add a link' => 'Legg til en relasjon',
'Add a new link' => 'Legg til en ny relasjon',
// 'Do you really want to remove this link: "%s"?' => '',
// 'Do you really want to remove this link with task #%d?' => '',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Aktivitetslogg',
'My calendar' => 'Min kalender',
'Search tasks' => 'Søk oppgave',
- 'Back to the calendar' => 'Tilbake til kalender',
- 'Filters' => 'Filtere',
'Reset filters' => 'Nullstill filter',
'My tasks due tomorrow' => 'Mine oppgaver med frist i morgen',
'Tasks due today' => 'Oppgaver med frist i dag',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Sluttdato:',
// 'There is no start date or end date for this project.' => '',
'Projects Gantt chart' => 'Gantt skjema for prosjekter',
- 'Link type' => 'Relasjonstype',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
'Milestone' => 'Milepæl',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ 'Link type' => 'Relasjonstype',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index 4fce1c33..18155816 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alle projecten',
'Add a new column' => 'Kolom toevoegen',
'Title' => 'Titel',
- 'Nobody assigned' => 'Niemand toegewezen',
'Assigned to %s' => 'Toegewezen aan %s',
'Remove a column' => 'Kolom verwijderen',
'Remove a column from a board' => 'Kolom verwijderen van het bord',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Aantal taken',
'User' => 'Gebruiker',
'Comments' => 'Commentaar',
- 'Write your text in Markdown' => 'Schrijf uw tekst in Markdown',
'Leave a comment' => 'Schrijf een commentaar',
'Comment is required' => 'Commentaar is verplicht',
'Leave a description' => 'Schrijf een omschrijving',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Tijdschrijven :',
'New sub-task' => 'Nieuwe subtaak',
'New attachment added "%s"' => 'Nieuwe bijlage toegevoegd « %s »',
- 'Comment updated' => 'Commentaar aangepast',
'New comment posted by %s' => 'Nieuw commentaar geplaatst door « %s »',
'New attachment' => 'Nieuwe bijlage',
'New comment' => 'Nieuw commentaar',
+ 'Comment updated' => 'Commentaar aangepast',
'New subtask' => 'Nieuwe subtaak',
'Subtask updated' => 'Subtaak aangepast',
'Task updated' => 'Taak aangepast',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formaat is altijd geaccepteerd, bijvoorbeeld : « %s » et « %s »',
'New private project' => 'Nieuw privé project',
'This project is private' => 'Dit project is privé',
- 'Type here to create a new sub-task' => 'Typ hier om een nieuwe subtaak aan te maken',
'Add' => 'Toevoegen',
'Start date' => 'Startdatum',
'Time estimated' => 'Geschatte tijd',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Dagelijkse project samenvatting voor « %s »',
'Exports' => 'Exports',
'This export contains the number of tasks per column grouped per day.' => 'Dit rapport bevat het aantal taken per kolom gegroupeerd per dag.',
- 'Nothing to preview...' => 'Niets om te previewen...',
- 'Preview' => 'Preview',
- 'Write' => 'Schrijf',
'Active swimlanes' => 'Actieve swinlanes',
'Add a new swimlane' => 'Nieuwe swimlane toevoegen',
'Change default swimlane' => 'Standaard swimlane aapassen',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Leeftijd taak in dagen',
'Days in this column' => 'Dagen in deze kolom',
'%dd' => '%dj',
- 'Add a link' => 'Link toevoegen',
'Add a new link' => 'Nieuwe link toevoegen',
'Do you really want to remove this link: "%s"?' => 'Weet u zeker dat u deze link wil verwijderen : « %s » ?',
'Do you really want to remove this link with task #%d?' => 'Weet u zeker dat u deze link met taak %d wil verwijderen?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Mijn activiteiten',
'My calendar' => 'Mijn kalender',
'Search tasks' => 'Zoek taken',
- 'Back to the calendar' => 'Terug naar de kalender',
- 'Filters' => 'Filters',
'Reset filters' => 'Reset filters',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
'Milestone' => 'Mijlpaal',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index 87201bd0..d9427d80 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Wszystkie projekty',
'Add a new column' => 'Dodaj nową kolumnę',
'Title' => 'Nazwa',
- 'Nobody assigned' => 'Nikt nie przypisany',
'Assigned to %s' => 'Przypisane do %s',
'Remove a column' => 'Usuń kolumnę',
'Remove a column from a board' => 'Usuń kolumnę z tablicy',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Liczba zadań',
'User' => 'Użytkownik',
'Comments' => 'Komentarze',
- 'Write your text in Markdown' => 'Zobacz jak formatować tekst z użyciem Markdown',
'Leave a comment' => 'Wstaw komentarz',
'Comment is required' => 'Komentarz jest wymagany',
'Leave a description' => 'Dodaj opis',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Śledzenie czasu: ',
'New sub-task' => 'Nowe Pod-zadanie',
'New attachment added "%s"' => 'Nowy załącznik dodany "%s"',
- 'Comment updated' => 'Komentarz zaktualizowany',
'New comment posted by %s' => 'Nowy komentarz dodany przez %s',
'New attachment' => 'Nowy załącznik',
'New comment' => 'Nowy Komentarz',
+ 'Comment updated' => 'Komentarz zaktualizowany',
'New subtask' => 'Nowe pod-zadanie',
'Subtask updated' => 'Zaktualizowane pod-zadanie',
'Task updated' => 'Zaktualizowane zadanie',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO jest zawsze akceptowany, przykłady: "%s", "%s"',
'New private project' => 'Nowy projekt prywatny',
'This project is private' => 'Ten projekt jest prywatny',
- 'Type here to create a new sub-task' => 'Nazwa podzadania',
'Add' => 'Dodaj',
'Start date' => 'Data rozpoczęcia',
'Time estimated' => 'Szacowany czas',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Wygeneruj dzienny raport dla projektu: "%s"',
'Exports' => 'Eksporty',
'This export contains the number of tasks per column grouped per day.' => 'Ten eksport zawiera ilość zadań zgrupowanych w kolumnach na dzień',
- 'Nothing to preview...' => 'Nic do podejrzenia...',
- 'Preview' => 'Podgląd',
- 'Write' => 'Edycja',
'Active swimlanes' => 'Aktywne procesy',
'Add a new swimlane' => 'Dodaj proces',
'Change default swimlane' => 'Zmień domyślny proces',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Wiek zadania w dniach',
'Days in this column' => 'Dni w tej kolumnie',
// '%dd' => '',
- 'Add a link' => 'Dodaj link',
'Add a new link' => 'Dodaj nowy link',
'Do you really want to remove this link: "%s"?' => 'Czy na pewno chcesz usunąć ten link: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Czy na pewno chcesz usunąć ten link razem z zadaniem nr %d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Moja aktywność',
'My calendar' => 'Mój kalendarz',
'Search tasks' => 'Szukaj zadań',
- 'Back to the calendar' => 'Wróć do kalendarza',
- 'Filters' => 'Filtry',
'Reset filters' => 'Resetuj zastosowane filtry',
'My tasks due tomorrow' => 'Moje zadania do jutra',
'Tasks due today' => 'Zadania do dzisiaj',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Data zakończenia:',
'There is no start date or end date for this project.' => 'Nie zdefiniowano czasu trwania projektu',
'Projects Gantt chart' => 'Wykres Gantta dla projektów',
- 'Link type' => 'Rodzaj link\'u',
'Change task color when using a specific task link' => 'Zmień kolor zadania używając specjalnego adresu URL',
'Task link creation or modification' => 'Adres URL do utworzenia zadania lub modyfikacji',
'Milestone' => 'Kamień milowy',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
'Owner' => 'Właściciel',
'Unread notifications' => 'Nieprzeczytane powiadomienia',
- 'My filters' => 'Moje filtry',
'Notification methods:' => 'Metody powiadomień:',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ 'Link type' => 'Rodzaj link\'u',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
'Auto' => 'Automatyczny',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Link zewnętrzny',
'Copy and paste your link here...' => 'Skopiuj i wklej link tutaj ...',
// 'URL' => '',
- 'There is no external link for the moment.' => 'Brak linków zewnętrznych.',
'Internal links' => 'Linki do innych zadań',
- 'There is no internal link for the moment.' => 'Brak powiązań do innych zadań.',
// 'Assign to me' => '',
'Me' => 'JA',
'Do not duplicate anything' => 'Nie kopiuj żadnego projektu',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Zarządzanie użytkownikami',
'Groups management' => 'Zarządzanie grupami',
'Create from another project' => 'Utwórz na podstawie innego projektu',
- 'There is no subtask at the moment.' => 'Brak podzadań.',
'open' => 'otwarty',
'closed' => 'zamknięty',
'Priority:' => 'Priorytet:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Rozpoczęte:',
'Moved:' => 'Przesunięcie:',
'Task #%d' => 'Zadanie #%d',
- 'Sub-tasks' => 'Podzadania',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
'Upload a file' => 'Prześlij plik',
- 'There is no attachment at the moment.' => 'Brak załączników.',
'View file' => 'Wyświetl plik',
'Last activity' => 'Ostatnia aktywność',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 2c9412b6..e0cdb17d 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Todos os projetos',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
- 'Nobody assigned' => 'Ninguém designado',
'Assigned to %s' => 'Designado para %s',
'Remove a column' => 'Remover uma coluna',
'Remove a column from a board' => 'Remover uma coluna do board',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Número de tarefas',
'User' => 'Usuário',
'Comments' => 'Comentários',
- 'Write your text in Markdown' => 'Escreva seu texto em Markdown',
'Leave a comment' => 'Deixe um comentário',
'Comment is required' => 'Comentário é obrigatório',
'Leave a description' => 'Deixe uma descrição',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Controle de tempo:',
'New sub-task' => 'Nova subtarefa',
'New attachment added "%s"' => 'Novo anexo adicionado "%s"',
- 'Comment updated' => 'Comentário atualizado',
'New comment posted by %s' => 'Novo comentário postado por %s',
'New attachment' => 'Novo anexo',
'New comment' => 'Novo comentário',
+ 'Comment updated' => 'Comentário atualizado',
'New subtask' => 'Nova subtarefa',
'Subtask updated' => 'Subtarefa alterada',
'Task updated' => 'Tarefa alterada',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceito, exemplo: "%s" e "%s"',
'New private project' => 'Novo projeto privado',
'This project is private' => 'Este projeto é privado',
- 'Type here to create a new sub-task' => 'Digite aqui para criar uma nova subtarefa',
'Add' => 'Adicionar',
'Start date' => 'Data de início',
'Time estimated' => 'Tempo estimado',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Exportação diária do resumo do projeto para "%s"',
'Exports' => 'Exportar',
'This export contains the number of tasks per column grouped per day.' => 'Esta exportação contém o número de tarefas por coluna agrupada por dia.',
- 'Nothing to preview...' => 'Nada para pré-visualizar...',
- 'Preview' => 'Pré-visualizar',
- 'Write' => 'Escrever',
'Active swimlanes' => 'Ativar swimlanes',
'Add a new swimlane' => 'Adicionar swimlane',
'Change default swimlane' => 'Alterar swimlane padrão',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Idade da tarefa em dias',
'Days in this column' => 'Dias nesta coluna',
'%dd' => '%dd',
- 'Add a link' => 'Adicionar uma associação',
'Add a new link' => 'Adicionar uma nova associação',
'Do you really want to remove this link: "%s"?' => 'Você realmente deseja remover esta associação: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Você realmente deseja remover esta associação com a tarefa #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Meu feed de atividades',
'My calendar' => 'Minha agenda',
'Search tasks' => 'Pesquisar tarefas',
- 'Back to the calendar' => 'Voltar ao calendário',
- 'Filters' => 'Filtros',
'Reset filters' => 'Redefinir os filtros',
'My tasks due tomorrow' => 'Minhas tarefas que expiram amanhã',
'Tasks due today' => 'Tarefas que expiram hoje',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Data de término:',
'There is no start date or end date for this project.' => 'Não há data de início ou data de término para este projeto.',
'Projects Gantt chart' => 'Gráfico de Gantt dos projetos',
- 'Link type' => 'Tipo de link',
'Change task color when using a specific task link' => 'Mudar a cor da tarefa quando um link específico é utilizado',
'Task link creation or modification' => 'Criação ou modificação de um link em uma tarefa',
'Milestone' => 'Milestone',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Compartilhado',
'Owner' => 'Líder',
'Unread notifications' => 'Notificações não lidas',
- 'My filters' => 'Meus filtros',
'Notification methods:' => 'Métodos de notificação:',
'Import tasks from CSV file' => 'Importar tarefas a partir de arquivo CSV',
'Unable to read your file' => 'Não foi possível ler seu arquivo',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Nomes de usuário devem ser únicos e em letras minúsculas',
'Passwords will be encrypted if present' => 'Senhas serão encriptadas, se presentes',
'%s attached a new file to the task %s' => '%s anexou um novo arquivo a tarefa %s',
+ 'Link type' => 'Tipo de link',
'Assign automatically a category based on a link' => 'Atribuir automaticamente uma categoria baseada num link',
'BAM - Konvertible Mark' => 'BAM - Mark conversível',
'Assignee Username' => 'Usuário designado',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Fechar uma tarefa sem atividade',
'Duration in days' => 'Duração em dias',
'Send email when there is no activity on a task' => 'Enviar um e-mail quando não há nenhuma atividade em uma tarefa',
- 'List of external links' => 'Lista dos links externos',
'Unable to fetch link information.' => 'Não foi possível obter informações sobre o link.',
'Daily background job for tasks' => 'Tarefa agendada diariamente para as tarefas',
'Auto' => 'Auto',
@@ -1061,94 +1050,121 @@ return array(
'Add external link' => 'Adicionar um link externo',
'Type' => 'Tipo',
'Dependency' => 'Dependência',
- // 'Add internal link' => '',
- // 'Add a new external link' => '',
- // 'Edit external link' => '',
- // 'External link' => '',
- // 'Copy and paste your link here...' => '',
- // 'URL' => '',
- // 'There is no external link for the moment.' => '',
- // 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
- // 'Assign to me' => '',
- // 'Me' => '',
- // 'Do not duplicate anything' => '',
- // 'Projects management' => '',
- // 'Users management' => '',
- // 'Groups management' => '',
- // 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
- // 'open' => '',
- // 'closed' => '',
- // 'Priority:' => '',
- // 'Reference:' => '',
- // 'Complexity:' => '',
- // 'Swimlane:' => '',
- // 'Column:' => '',
- // 'Position:' => '',
- // 'Creator:' => '',
- // 'Time estimated:' => '',
- // '%s hours' => '',
- // 'Time spent:' => '',
- // 'Created:' => '',
- // 'Modified:' => '',
- // 'Completed:' => '',
- // 'Started:' => '',
- // 'Moved:' => '',
- // 'Task #%d' => '',
- // 'Sub-tasks' => '',
- // 'Date and time format' => '',
- // 'Time format' => '',
- // 'Start date: ' => '',
- // 'End date: ' => '',
- // 'New due date: ' => '',
- // 'Start date changed: ' => '',
- // 'Disable private projects' => '',
- // 'Do you really want to remove this custom filter: "%s"?' => '',
- // 'Remove a custom filter' => '',
- // 'User activated successfully.' => '',
- // 'Unable to enable this user.' => '',
- // 'User disabled successfully.' => '',
- // 'Unable to disable this user.' => '',
- // 'All files have been uploaded successfully.' => '',
- // 'View uploaded files' => '',
- // 'The maximum allowed file size is %sB.' => '',
- // 'Choose files again' => '',
- // 'Drag and drop your files here' => '',
- // 'choose files' => '',
- // 'View profile' => '',
- // 'Two Factor' => '',
- // 'Disable user' => '',
- // 'Do you really want to disable this user: "%s"?' => '',
- // 'Enable user' => '',
- // 'Do you really want to enable this user: "%s"?' => '',
- // 'Download' => '',
- // 'Uploaded: %s' => '',
- // 'Size: %s' => '',
- // 'Uploaded by %s' => '',
- // 'Filename' => '',
- // 'Size' => '',
- // 'Column created successfully.' => '',
- // 'Another column with the same name exists in the project' => '',
- // 'Default filters' => '',
- // 'Your board doesn\'t have any column!' => '',
- // 'Change column position' => '',
- // 'Switch to the project overview' => '',
- // 'User filters' => '',
- // 'Category filters' => '',
- // 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
- // 'View file' => '',
- // 'Last activity' => '',
- // 'Change subtask position' => '',
- // 'This value must be greater than %d' => '',
- // 'Another swimlane with the same name exists in the project' => '',
- // 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => '',
- // 'Actions duplicated successfully.' => '',
- // 'Unable to duplicate actions.' => '',
- // 'Add a new action' => '',
- // 'Import from another project' => '',
- // 'There is no action at the moment.' => '',
- // 'Import actions from another project' => '',
- // 'There is no available project.' => '',
+ 'Add internal link' => 'Adicionar um link interno',
+ 'Add a new external link' => 'Adicionar um novo link externo',
+ 'Edit external link' => 'Editar um link externo',
+ 'External link' => 'Link externo',
+ 'Copy and paste your link here...' => 'Copie e cole o link aqui...',
+ 'URL' => 'URL',
+ 'Internal links' => 'Link interno',
+ 'Assign to me' => 'Atribuir-me',
+ 'Me' => 'Eu',
+ 'Do not duplicate anything' => 'Não duplique nada',
+ 'Projects management' => 'Gestão de projetos',
+ 'Users management' => 'Gestão dos usuários',
+ 'Groups management' => 'Gestão dos grupos',
+ 'Create from another project' => 'Criar a partir de outro projeto',
+ 'open' => 'aberto',
+ 'closed' => 'fechado',
+ 'Priority:' => 'Prioridade:',
+ 'Reference:' => 'Referência:',
+ 'Complexity:' => 'Complexidade:',
+ 'Swimlane:' => 'Swimlane:',
+ 'Column:' => 'Coluna:',
+ 'Position:' => 'Posição:',
+ 'Creator:' => 'Criador:',
+ 'Time estimated:' => 'Tempo estimado:',
+ '%s hours' => '%s horas',
+ 'Time spent:' => 'Tempo gasto:',
+ 'Created:' => 'Criado:',
+ 'Modified:' => 'Modificado:',
+ 'Completed:' => 'Completado:',
+ 'Started:' => 'Começado:',
+ 'Moved:' => 'Movido:',
+ 'Task #%d' => 'Tarefa #%d',
+ 'Date and time format' => 'Formato da hora e da data',
+ 'Time format' => 'Formato da hora',
+ 'Start date: ' => 'Data de início: ',
+ 'End date: ' => 'Data final: ',
+ 'New due date: ' => 'Nova data limite: ',
+ 'Start date changed: ' => 'Data de início alterada: ',
+ 'Disable private projects' => 'Desativar os projetos privados',
+ 'Do you really want to remove this custom filter: "%s"?' => 'Você realmente quer remover este filtro personalizado: "%s"?',
+ 'Remove a custom filter' => 'Remover um filtro personalizado',
+ 'User activated successfully.' => 'Usuário ativado com sucesso.',
+ 'Unable to enable this user.' => 'Impossível de ativar esse usuário.',
+ 'User disabled successfully.' => 'Usuário desactivado com sucesso.',
+ 'Unable to disable this user.' => 'Impossível de desativar esse usuário.',
+ 'All files have been uploaded successfully.' => 'Todos os arquivos foram enviados com sucesso.',
+ 'View uploaded files' => 'Ver os arquivos enviados',
+ 'The maximum allowed file size is %sB.' => 'O tamanho máximo dos arquivos é %sB.',
+ 'Choose files again' => 'Selecionar novamente arquivos',
+ 'Drag and drop your files here' => 'Arraste e solte os arquivos aqui',
+ 'choose files' => 'selecione os arquivos',
+ 'View profile' => 'Ver o perfil',
+ 'Two Factor' => 'Dois fatores',
+ 'Disable user' => 'Desativar o usuário',
+ 'Do you really want to disable this user: "%s"?' => 'Você realmente quer desativar este usuário: "%s"?',
+ 'Enable user' => 'Ativar um usuário',
+ 'Do you really want to enable this user: "%s"?' => 'Você realmente quer ativar este usuário: "%s"?',
+ 'Download' => 'Baixar',
+ 'Uploaded: %s' => 'Enviado: %s',
+ 'Size: %s' => 'Tamanho: %s',
+ 'Uploaded by %s' => 'Enviado por %s',
+ 'Filename' => 'Nome do arquivo',
+ 'Size' => 'Tamanho',
+ 'Column created successfully.' => 'A coluna criada com sucesso.',
+ 'Another column with the same name exists in the project' => 'Uma outra coluna com o mesmo nome já existe no projeto',
+ 'Default filters' => 'Filtros padrão',
+ 'Your board doesn\'t have any column!' => 'O seu painel não tem nenhuma coluna',
+ 'Change column position' => 'Alterar a posição da coluna',
+ 'Switch to the project overview' => 'Mudar para a vista geral do projeto',
+ 'User filters' => 'Filtros dos usuários',
+ 'Category filters' => 'Filtros das categorias',
+ 'Upload a file' => 'Enviar um arquivo',
+ 'View file' => 'Ver um arquivo',
+ 'Last activity' => 'Últimas atividades',
+ 'Change subtask position' => 'Alterar a posição da sub-tarefa',
+ 'This value must be greater than %d' => 'Este valor deve ser maior que %d',
+ 'Another swimlane with the same name exists in the project' => 'Outra Swimlane existe com o mesmo nome no projeto',
+ 'Example: http://example.kanboard.net/ (used to generate absolute URLs)' => 'Exemplo: http://exemple.kanboard.net/ (usado para gerar URLs absolutos)',
+ 'Actions duplicated successfully.' => 'Ações duplicadas com sucesso.',
+ 'Unable to duplicate actions.' => 'Não foi possível duplicar as ações.',
+ 'Add a new action' => 'Adicionar uma nova ação',
+ 'Import from another project' => 'Importar a partir de outro projeto',
+ 'There is no action at the moment.' => 'Não há nenhuma ação actualmente.',
+ 'Import actions from another project' => 'Importar ações a partir de outro projeto',
+ 'There is no available project.' => 'Não há projetos disponíveis.',
+ 'Local File' => 'Arquivo local',
+ 'Configuration' => 'Configuração',
+ 'PHP version:' => 'Versão do PHP:',
+ 'PHP SAPI:' => 'PHP SAPI:',
+ 'OS version:' => 'Versão do sistema operacional:',
+ 'Database version:' => 'Versão do banco de dados:',
+ 'Browser:' => 'Browser:',
+ 'Task view' => 'Vista detalhada de uma tarefa',
+ 'Edit task' => 'Editar a tarefa',
+ 'Edit description' => 'Editar a descrição',
+ 'New internal link' => 'Novo link interno',
+ 'Display list of keyboard shortcuts' => 'Ver a lista dos atalhos de teclado',
+ 'Menu' => 'Menu',
+ 'Set start date' => 'Definir a data de início',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Enviar a minha imagem de avatar',
+ 'Remove my image' => 'Remover a minha imagem',
+ 'The OAuth2 state parameter is invalid' => 'O parâmetro "state" de OAuth2 não é válido',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php
index 31a56882..aa51534b 100644
--- a/app/Locale/pt_PT/translations.php
+++ b/app/Locale/pt_PT/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Todos os projectos',
'Add a new column' => 'Adicionar uma nova coluna',
'Title' => 'Título',
- 'Nobody assigned' => 'Ninguém assignado',
'Assigned to %s' => 'Designado para %s',
'Remove a column' => 'Remover uma coluna',
'Remove a column from a board' => 'Remover uma coluna do quadro',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Número de tarefas',
'User' => 'Utilizador',
'Comments' => 'Comentários',
- 'Write your text in Markdown' => 'Escreva o seu texto em Markdown',
'Leave a comment' => 'Deixe um comentário',
'Comment is required' => 'Comentário é obrigatório',
'Leave a description' => 'Deixe uma descrição',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Controle de tempo:',
'New sub-task' => 'Nova subtarefa',
'New attachment added "%s"' => 'Novo anexo adicionado "%s"',
- 'Comment updated' => 'Comentário actualizado',
'New comment posted by %s' => 'Novo comentário por %s',
'New attachment' => 'Novo anexo',
'New comment' => 'Novo comentário',
+ 'Comment updated' => 'Comentário actualizado',
'New subtask' => 'Nova subtarefa',
'Subtask updated' => 'Subtarefa alterada',
'Task updated' => 'Tarefa alterada',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceite, exemplo: "%s" e "%s"',
'New private project' => 'Novo projecto privado',
'This project is private' => 'Este projecto é privado',
- 'Type here to create a new sub-task' => 'Escreva aqui para criar uma nova subtarefa',
'Add' => 'Adicionar',
'Start date' => 'Data de início',
'Time estimated' => 'Tempo estimado',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Exportação diária do resumo do projecto para "%s"',
'Exports' => 'Exportar',
'This export contains the number of tasks per column grouped per day.' => 'Esta exportação contém o número de tarefas por coluna agrupada por dia.',
- 'Nothing to preview...' => 'Nada para pré-visualizar...',
- 'Preview' => 'Pré-visualizar',
- 'Write' => 'Escrever',
'Active swimlanes' => 'Activar swimlanes',
'Add a new swimlane' => 'Adicionar novo swimlane',
'Change default swimlane' => 'Alterar swimlane padrão',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Idade da tarefa em dias',
'Days in this column' => 'Dias nesta coluna',
// '%dd' => '',
- 'Add a link' => 'Adicionar uma associação',
'Add a new link' => 'Adicionar uma nova associação',
'Do you really want to remove this link: "%s"?' => 'Tem a certeza que quer remover esta associação: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Tem a certeza que quer remover esta associação com a tarefa n°%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'O meu feed de actividade',
'My calendar' => 'A minha agenda',
'Search tasks' => 'Pesquisar tarefas',
- 'Back to the calendar' => 'Voltar ao calendário',
- 'Filters' => 'Filtros',
'Reset filters' => 'Redefinir os filtros',
'My tasks due tomorrow' => 'A minhas tarefas que expiram amanhã',
'Tasks due today' => 'Tarefas que expiram hoje',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Data de fim:',
'There is no start date or end date for this project.' => 'Não existe data de inicio ou fim para este projecto.',
'Projects Gantt chart' => 'Gráfico de Gantt dos projectos',
- 'Link type' => 'Tipo de ligação',
'Change task color when using a specific task link' => 'Alterar cor da tarefa quando se usar um tipo especifico de ligação de tarefa',
'Task link creation or modification' => 'Criação ou modificação de ligação de tarefa',
'Milestone' => 'Objectivo',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Partilhado',
'Owner' => 'Dono',
'Unread notifications' => 'Notificações por ler',
- 'My filters' => 'Os meus filtros',
'Notification methods:' => 'Métodos de notificação:',
'Import tasks from CSV file' => 'Importar tarefas de um ficheiro CSV',
'Unable to read your file' => 'Não foi possivel ler o ficheiro',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Utilizadores tem de estar em letra pequena e ser unicos',
'Passwords will be encrypted if present' => 'Senhas serão encriptadas se presentes',
'%s attached a new file to the task %s' => '%s anexou um novo ficheiro à tarefa %s',
+ 'Link type' => 'Tipo de ligação',
'Assign automatically a category based on a link' => 'Assignar automáticamente a categoria baseada num link',
'BAM - Konvertible Mark' => 'BAM - Marca Conversível',
'Assignee Username' => 'Utilizador do Assignado',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Fechar tarefa quando não há actividade',
'Duration in days' => 'Duração em dias',
'Send email when there is no activity on a task' => 'Enviar email quando não há actividade numa tarefa',
- 'List of external links' => 'Lista de ligações externas',
'Unable to fetch link information.' => 'Impossivel obter informação da ligação.',
'Daily background job for tasks' => 'Trabalho diário em segundo plano para tarefas',
'Auto' => 'Auto',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Ligação externa',
'Copy and paste your link here...' => 'Copie e cole a sua ligação aqui...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'De momento não existe nenhuma ligação externa.',
'Internal links' => 'Ligações internas',
- 'There is no internal link for the moment.' => 'De momento não existe nenhuma ligação interna.',
'Assign to me' => 'Assignar a mim',
'Me' => 'Eu',
'Do not duplicate anything' => 'Não duplicar nada',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Gestão de utilizadores',
'Groups management' => 'Gestão de grupos',
'Create from another project' => 'Criar apartir de outro projecto',
- 'There is no subtask at the moment.' => 'De momento não existe sub-tarefa.',
'open' => 'aberto',
'closed' => 'fechado',
'Priority:' => 'Prioridade:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Iniciado:',
'Moved:' => 'Movido:',
'Task #%d' => 'Tarefa #%d',
- 'Sub-tasks' => 'Sub-tarefa',
'Date and time format' => 'Formato tempo e data',
'Time format' => 'Formato tempo',
'Start date: ' => 'Data inicio: ',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Filtros de utilizador',
'Category filters' => 'Filtros de categoria',
'Upload a file' => 'Enviar um ficheiro',
- 'There is no attachment at the moment.' => 'De momento não existe anexo.',
'View file' => 'Ver ficheiro',
'Last activity' => 'Ultima actividade',
'Change subtask position' => 'Mudar posição da sub-tarefa',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'De momento não existe acção.',
'Import actions from another project' => 'Importar acções de outro projecto',
'There is no available project.' => 'Não existe projecto disponivel.',
+ 'Local File' => 'Ficheiro Local',
+ 'Configuration' => 'Configuração',
+ 'PHP version:' => 'Versão PHP:',
+ 'PHP SAPI:' => 'SAPI PHP:',
+ 'OS version:' => 'Versão SO:',
+ 'Database version:' => 'Versão base de dados:',
+ 'Browser:' => 'Navegador:',
+ 'Task view' => 'Vista de Tarefas',
+ 'Edit task' => 'Editar tarefa',
+ 'Edit description' => 'Editar descrição',
+ 'New internal link' => 'Nova ligação interna',
+ 'Display list of keyboard shortcuts' => 'Mostrar lista de atalhos do teclado',
+ 'Menu' => 'Menu',
+ 'Set start date' => 'Definir data de inicio',
+ 'Avatar' => 'Avatar',
+ 'Upload my avatar image' => 'Enviar a minha imagem de avatar',
+ 'Remove my image' => 'Remover a minha imagem',
+ 'The OAuth2 state parameter is invalid' => 'O parametro de estado do OAuth2 é inválido',
+ 'User not found.' => 'Utilizador não encontrado.',
+ 'Search in activity stream' => 'Procurar no fluxo de atividade',
+ 'My activities' => 'Minhas actividades',
+ 'Activity until yesterday' => 'Actividade até ontem',
+ 'Activity until today' => 'Actividade até hoje',
+ 'Search by creator: ' => 'Procurar por criador: ',
+ 'Search by creation date: ' => 'Procurar por data de criação: ',
+ 'Search by task status: ' => 'Procurar por estado da tarefa: ',
+ 'Search by task title: ' => 'Procurar por titulo da tarefa: ',
+ 'Activity stream search' => 'Procurar fluxo de actividade',
+ 'Projects where "%s" is manager' => 'Projectos onde "%s" é gestor',
+ 'Projects where "%s" is member' => 'Projectos onde "%s" é membro',
+ 'Open tasks assigned to "%s"' => 'Tarefas abertas assignadas a "%s"',
+ 'Closed tasks assigned to "%s"' => 'Tarefas fechadas assignadas a "%s"',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 74f1deb8..bf2bc559 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Все проекты',
'Add a new column' => 'Добавить новую колонку',
'Title' => 'Название',
- 'Nobody assigned' => 'Никто не назначен',
'Assigned to %s' => 'Назначено %s',
'Remove a column' => 'Удалить колонку',
'Remove a column from a board' => 'Удалить колонку с доски',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Количество задач',
'User' => 'Пользователь',
'Comments' => 'Комментарии',
- 'Write your text in Markdown' => 'Справка по синтаксису Markdown',
'Leave a comment' => 'Оставить комментарий',
'Comment is required' => 'Нужен комментарий',
'Leave a description' => 'Напишите описание',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Отслеживание времени:',
'New sub-task' => 'Новая подзадача',
'New attachment added "%s"' => 'Добавлено вложение « %s »',
- 'Comment updated' => 'Комментарий обновлен',
'New comment posted by %s' => 'Новый комментарий написан « %s »',
'New attachment' => 'Новое вложение',
'New comment' => 'Новый комментарий',
+ 'Comment updated' => 'Комментарий обновлен',
'New subtask' => 'Новая подзадача',
'Subtask updated' => 'Подзадача обновлена',
'Task updated' => 'Задача обновлена',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Время должно быть в ISO-формате, например: "%s" или "%s"',
'New private project' => 'Новый проект с ограниченным доступом',
'This project is private' => 'Это проект с ограниченным доступом',
- 'Type here to create a new sub-task' => 'Печатайте сюда чтобы создать подзадачу',
'Add' => 'Добавить',
'Start date' => 'Дата начала',
'Time estimated' => 'Запланировано',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Экспорт ежедневного резюме проекта "%s"',
'Exports' => 'Экспорт',
'This export contains the number of tasks per column grouped per day.' => 'Этот экспорт содержит ряд задач в колонках, сгруппированные по дням.',
- 'Nothing to preview...' => 'Нет данных для предпросмотра...',
- 'Preview' => 'Предпросмотр',
- 'Write' => 'Написание',
'Active swimlanes' => 'Активные дорожки',
'Add a new swimlane' => 'Добавить новую дорожку',
'Change default swimlane' => 'Сменить стандартную дорожку',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Возраст задачи в днях',
'Days in this column' => 'Дней в этой колонке',
'%dd' => '%dd',
- 'Add a link' => 'Добавить ссылку на другие задачи',
'Add a new link' => 'Добавление новой ссылки',
'Do you really want to remove this link: "%s"?' => 'Вы уверены что хотите удалить ссылку: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Вы уверены что хотите удалить ссылку вместе с задачей #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Лента моей активности',
'My calendar' => 'Мой календарь',
'Search tasks' => 'Поиск задачи',
- 'Back to the calendar' => 'Вернуться в календарь',
- 'Filters' => 'Фильтры',
'Reset filters' => 'Сбросить фильтры',
'My tasks due tomorrow' => 'Мои задачи на завтра',
'Tasks due today' => 'Задачи, завершающиеся сегодня',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Дата завершения:',
'There is no start date or end date for this project.' => 'В проекте не указаны дата начала или завершения.',
'Projects Gantt chart' => 'Диаграмма Ганта проектов',
- 'Link type' => 'Тип ссылки',
'Change task color when using a specific task link' => 'Изменение цвета задач при использовании ссылки на определенные задачи',
'Task link creation or modification' => 'Ссылка на создание или модификацию задачи',
'Milestone' => 'Веха',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Общие',
'Owner' => 'Владелец',
'Unread notifications' => 'Непрочитанные уведомления',
- 'My filters' => 'Мои фильтры',
'Notification methods:' => 'Способы уведомления:',
'Import tasks from CSV file' => 'Импорт задач из CSV-файла',
'Unable to read your file' => 'Невозможно прочитать файл',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Логины пользователей должны быть строчными и уникальными',
'Passwords will be encrypted if present' => 'Пароли будут зашифрованы (если указаны)',
'%s attached a new file to the task %s' => '%s добавил новый файл к задаче %s',
+ 'Link type' => 'Тип ссылки',
'Assign automatically a category based on a link' => 'Автоматически назначать категории на основе ссылки',
'BAM - Konvertible Mark' => 'BAM - Конвертируемая марка',
'Assignee Username' => 'Логин назначенного',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'Закрывать задачу, когда нет активности',
'Duration in days' => 'Длительность в днях',
'Send email when there is no activity on a task' => 'Отправлять email, когда активность по задаче отсутствует',
- 'List of external links' => 'Список внешних ссылок',
'Unable to fetch link information.' => 'Не удалось получить информацию о ссылке',
'Daily background job for tasks' => 'Ежедневные фоновые работы для задач',
'Auto' => 'Авто',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'Внешняя ссылка',
'Copy and paste your link here...' => 'Скопируйте и вставьте вашу ссылку здесь',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'На данный момент внешние ссылки отсутствуют',
'Internal links' => 'Внутренние ссылки',
- 'There is no internal link for the moment.' => 'На данные момент внутреннии ссылки отсутствуют',
'Assign to me' => 'Связать со мной',
'Me' => 'Мне',
'Do not duplicate anything' => 'Не дублировать ничего',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'Управление пользователями',
'Groups management' => 'Управление группами',
'Create from another project' => 'Создать из другого проекта',
- 'There is no subtask at the moment.' => 'На данный момент подзадачи отсутствуют',
'open' => 'открыто',
'closed' => 'закрыто',
'Priority:' => 'Приоритет:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'Начата:',
'Moved:' => 'Перемещена:',
'Task #%d' => 'Задача #%d',
- 'Sub-tasks' => 'Подзадачи',
'Date and time format' => 'Формат даты и времени',
'Time format' => 'Формат времени',
'Start date: ' => 'Дата начала:',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => 'Фильтры по пользователям',
'Category filters' => 'Фильтры по категориям',
'Upload a file' => 'Загрузить файл',
- 'There is no attachment at the moment.' => 'Вложения на данный момент отсутствуют',
'View file' => 'Просмотр файла',
'Last activity' => 'Последняя активность',
'Change subtask position' => 'Смена позиции подзадачи',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => 'Действия на данный момент отсутствуют',
'Import actions from another project' => 'Импорт действий из другого проекта',
'There is no available project.' => 'Нет доступного проекта',
+ 'Local File' => 'Локальный файл',
+ 'Configuration' => 'Конфигурация',
+ 'PHP version:' => 'Версия PHP:',
+ 'PHP SAPI:' => 'PHP SAPI:',
+ 'OS version:' => 'Версия ОС:',
+ 'Database version:' => 'Версия БД:',
+ 'Browser:' => 'Браузер:',
+ 'Task view' => 'Просмотр задачи',
+ 'Edit task' => 'Изменение задачи',
+ 'Edit description' => 'Изменение описания',
+ 'New internal link' => 'Новая внутренняя ссылка',
+ 'Display list of keyboard shortcuts' => 'Показать список клавиатурных сокращений',
+ 'Menu' => 'Меню',
+ 'Set start date' => 'Установить дату начала',
+ 'Avatar' => 'Аватар',
+ 'Upload my avatar image' => 'Загрузить моё изображение для аватара',
+ 'Remove my image' => 'Удалить моё изображение',
+ 'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index e63a106c..0399530e 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Svi projekti',
'Add a new column' => 'Dodaj novu kolonu',
'Title' => 'Naslov',
- 'Nobody assigned' => 'Niko nije dodeljen',
'Assigned to %s' => 'Dodeljen korisniku %s',
'Remove a column' => 'Ukloni kolonu',
'Remove a column from a board' => 'Ukloni kolonu sa table',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Broj zadataka',
'User' => 'Korisnik',
'Comments' => 'Komentari',
- 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown',
'Leave a comment' => 'Ostavi komentar',
'Comment is required' => 'Komentar je obavezan',
'Leave a description' => 'Dodaj opis',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Praćenje vremena: ',
'New sub-task' => 'Novi Pod-zadatak',
'New attachment added "%s"' => 'Novi prilog ubačen "%s"',
- 'Comment updated' => 'Komentar izmenjen',
'New comment posted by %s' => 'Novi komentar ostavio %s',
// 'New attachment' => '',
// 'New comment' => '',
+ 'Comment updated' => 'Komentar izmenjen',
// 'New subtask' => '',
// 'Subtask updated' => '',
// 'Task updated' => '',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvek prihvatljiv, primer: "%s", "%s"',
'New private project' => 'Novi privatni projekat',
'This project is private' => 'Ovaj projekat je privatan',
- 'Type here to create a new sub-task' => 'Kucaj ovde za kreiranje novog pod-zadatka',
'Add' => 'Dodaj',
'Start date' => 'Datum početka',
'Time estimated' => 'Procenjeno vreme',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Izvoz zbirnig pregleda po danima za "%s"',
'Exports' => 'Izvoz',
// 'This export contains the number of tasks per column grouped per day.' => '',
- 'Nothing to preview...' => 'Ništa za prikazivanje...',
- 'Preview' => 'Pregled',
- 'Write' => 'Piši',
'Active swimlanes' => 'Aktivni razdelnik',
'Add a new swimlane' => 'Dodaj razdelnik',
'Change default swimlane' => 'Zameni osnovni razdelnik',
@@ -539,7 +533,6 @@ return array(
// 'Task age in days' => '',
// 'Days in this column' => '',
// '%dd' => '',
- 'Add a link' => 'Dodaj link',
// 'Add a new link' => '',
// 'Do you really want to remove this link: "%s"?' => '',
// 'Do you really want to remove this link with task #%d?' => '',
@@ -749,8 +742,6 @@ return array(
// 'My activity stream' => '',
// 'My calendar' => '',
// 'Search tasks' => '',
- // 'Back to the calendar' => '',
- // 'Filters' => '',
// 'Reset filters' => '',
// 'My tasks due tomorrow' => '',
// 'Tasks due today' => '',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index 26952355..7e738e70 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Alla projekt',
'Add a new column' => 'Lägg till ny kolumn',
'Title' => 'Titel',
- 'Nobody assigned' => 'Ingen tilldelad',
'Assigned to %s' => 'Tilldelad %s',
'Remove a column' => 'Ta bort en kolumn',
'Remove a column from a board' => 'Ta bort en kolumn från tavlan',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Antal uppgifter',
'User' => 'Användare',
'Comments' => 'Kommentarer',
- 'Write your text in Markdown' => 'Exempelsyntax för text',
'Leave a comment' => 'Lämna en kommentar',
'Comment is required' => 'En kommentar måste lämnas',
'Leave a description' => 'Lämna en beskrivning',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Tidsspårning',
'New sub-task' => 'Ny deluppgift',
'New attachment added "%s"' => 'Ny bifogning tillagd "%s"',
- 'Comment updated' => 'Kommentaren har uppdaterats',
'New comment posted by %s' => 'Ny kommentar postad av %s',
'New attachment' => 'Ny bifogning',
'New comment' => 'Ny kommentar',
+ 'Comment updated' => 'Kommentaren har uppdaterats',
'New subtask' => 'Ny deluppgift',
'Subtask updated' => 'Deluppgiften har uppdaterats',
'Task updated' => 'Uppgiften har uppdaterats',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-format är alltid tillåtet, exempel: "%s" och "%s"',
'New private project' => 'Nytt privat projekt',
'This project is private' => 'Det här projektet är privat',
- 'Type here to create a new sub-task' => 'Skriv här för att skapa en ny deluppgift',
'Add' => 'Lägg till',
'Start date' => 'Startdatum',
'Time estimated' => 'Uppskattad tid',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'Export av daglig projektsummering för "%s"',
'Exports' => 'Exporter',
'This export contains the number of tasks per column grouped per day.' => 'Denna export innehåller antalet uppgifter per kolumn grupperade per dag.',
- 'Nothing to preview...' => 'Inget att förhandsgrandska...',
- 'Preview' => 'Förhandsgranska',
- 'Write' => 'Skriva',
'Active swimlanes' => 'Aktiva swimlanes',
'Add a new swimlane' => 'Lägg till en nytt swimlane',
'Change default swimlane' => 'Ändra standard swimlane',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Uppgiftsålder i dagar',
'Days in this column' => 'Dagar i denna kolumn',
'%dd' => '%dd',
- 'Add a link' => 'Lägg till länk',
'Add a new link' => 'Lägg till ny länk',
'Do you really want to remove this link: "%s"?' => 'Vill du verkligen ta bort länken: "%s"?',
'Do you really want to remove this link with task #%d?' => 'Vill du verkligen ta bort länken till uppgiften #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Min aktivitetsström',
'My calendar' => 'Min kalender',
'Search tasks' => 'Sök uppgifter',
- 'Back to the calendar' => 'Tillbaka till kalendern',
- 'Filters' => 'Filter',
'Reset filters' => 'Återställ filter',
'My tasks due tomorrow' => 'Mina uppgifter förfaller imorgon',
'Tasks due today' => 'Uppgifter förfaller idag',
@@ -850,7 +841,6 @@ return array(
// 'End date:' => '',
// 'There is no start date or end date for this project.' => '',
// 'Projects Gantt chart' => '',
- // 'Link type' => '',
// 'Change task color when using a specific task link' => '',
// 'Task link creation or modification' => '',
// 'Milestone' => '',
@@ -902,7 +892,6 @@ return array(
// 'Shared' => '',
// 'Owner' => '',
// 'Unread notifications' => '',
- // 'My filters' => '',
// 'Notification methods:' => '',
// 'Import tasks from CSV file' => '',
// 'Unable to read your file' => '',
@@ -940,6 +929,7 @@ return array(
// 'Usernames must be lowercase and unique' => '',
// 'Passwords will be encrypted if present' => '',
// '%s attached a new file to the task %s' => '',
+ // 'Link type' => '',
// 'Assign automatically a category based on a link' => '',
// 'BAM - Konvertible Mark' => '',
// 'Assignee Username' => '',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 12f374b6..6765e8ea 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'โปรเจคทั้งหมด',
'Add a new column' => 'เพิ่มคอลัมน์ใหม่',
'Title' => 'หัวเรื่อง',
- 'Nobody assigned' => 'ไม่กำหนดใคร',
'Assigned to %s' => 'กำหนดให้ %s',
'Remove a column' => 'ลบคอลัมน์',
'Remove a column from a board' => 'ลบคอลัมน์ออกจากบอร์ด',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'นับงาน',
'User' => 'ผู้ใช้',
'Comments' => 'ความคิดเห็น',
- 'Write your text in Markdown' => 'เขียนข้อความในรูปแบบ Markdown',
'Leave a comment' => 'ออกความคิดเห็น',
'Comment is required' => 'ต้องการความคิดเห็น',
'Leave a description' => 'แสดงคำอธิบาย',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'การติดตามเวลา:',
'New sub-task' => 'งานย่อยใหม่',
'New attachment added "%s"' => 'เพิ่มการแนบใหม่ "%s"',
- 'Comment updated' => 'ปรับปรุงความคิดเห็น',
'New comment posted by %s' => 'ความคิดเห็นใหม่จาก %s',
'New attachment' => 'การแนบใหม่',
'New comment' => 'ความคิดเห็นใหม่',
+ 'Comment updated' => 'ปรับปรุงความคิดเห็น',
'New subtask' => 'งานย่อยใหม่',
'Subtask updated' => 'ปรับปรุงงานย่อยแล้ว',
'Task updated' => 'ปรับปรุงงานแล้ว',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ยอมรับรูปแบบ ISO ตัวอย่าง: "%s" และ "%s"',
'New private project' => 'เพิ่มโปรเจคส่วนตัวใหม่',
'This project is private' => 'โปรเจคนี้เป็นโปรเจคส่วนตัว',
- 'Type here to create a new sub-task' => 'พิมพ์ที่นี้เพื่อสร้างงานย่อยใหม่',
'Add' => 'เพิ่ม',
'Start date' => 'เริ่มวันที่',
'Time estimated' => 'เวลาโดยประมาณ',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => 'ส่งออกสรุปโปรเจครายวันสำหรับ "%s"',
'Exports' => 'ส่งออก',
'This export contains the number of tasks per column grouped per day.' => 'การส่งออกนี้เป็นการนับจำนวนงานในแต่ละคอลัมน์ในแต่ละวัน',
- 'Nothing to preview...' => 'ไม่มีพรีวิว...',
- 'Preview' => 'พรีวิว',
- 'Write' => 'เขียน',
'Active swimlanes' => 'สวิมเลนพร้อมใช้งาน',
'Add a new swimlane' => 'เพิ่มสวิมเลนใหม่',
'Change default swimlane' => 'เปลี่ยนสวิมเลนเริ่มต้น',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'อายุงาน',
'Days in this column' => 'วันในคอลัมน์นี้',
'%dd' => '%d วัน',
- 'Add a link' => 'เพิ่มลิงค์',
'Add a new link' => 'เพิ่มลิงค์ใหม่',
'Do you really want to remove this link: "%s"?' => 'คุณต้องการลบลิงค์นี้: "%s"?',
'Do you really want to remove this link with task #%d?' => 'คุณต้องการลบลิงค์นี้ของงาน #%d?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'กิจกรรมที่เกิดขึ้นของฉัน',
'My calendar' => 'ปฎิทินของฉัน',
'Search tasks' => 'ค้นหางาน',
- 'Back to the calendar' => 'กลับไปที่ปฎิทิน',
- 'Filters' => 'ตัวกรอง',
'Reset filters' => 'ล้างตัวกรอง',
'My tasks due tomorrow' => 'งานถึงกำหนดของฉันวันพรุ่งนี้',
'Tasks due today' => 'งานถึงกำหนดวันนี้',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'วันที่จบ:',
'There is no start date or end date for this project.' => 'ไม่มีวันที่เริ่มหรือวันที่จบของโปรเจคนี้',
'Projects Gantt chart' => 'แผนภูมิแกรน์ของโปรเจค',
- 'Link type' => 'ประเภทลิงค์',
'Change task color when using a specific task link' => 'เปลี่ยนสีงานเมื่อมีการใช้การเชื่อมโยงงาน',
'Task link creation or modification' => 'การสร้างการเชื่อมโยงงานหรือการปรับเปลี่ยน',
'Milestone' => 'ขั้น',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'แชร์',
'Owner' => 'เจ้าของ',
'Unread notifications' => 'การแจ้งเตือนยังไม่ได้อ่าน',
- 'My filters' => 'ตัวกรองของฉัน',
'Notification methods:' => 'ลักษณะการแจ้งเตือน:',
'Import tasks from CSV file' => 'นำเข้างานจากไฟล์ CSV',
'Unable to read your file' => 'ไม่สามารถอ่านไฟล์ของคุณ',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'ชื่อผู้ใช้ต้องเป็นตัวพิมพ์เล็กและไม่ซ้ำ',
// 'Passwords will be encrypted if present' => '',
'%s attached a new file to the task %s' => '%s แนบไฟล์ใหม่ในงาน %s',
+ 'Link type' => 'ประเภทลิงค์',
'Assign automatically a category based on a link' => 'กำหนดหมวดอัตโนมัติตามลิงค์',
// 'BAM - Konvertible Mark' => '',
'Assignee Username' => 'กำหนดชื่อผู้ใช้',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => 'ปิดงานเมื่อไม่มีกิจกกรมเกิดขึ้น',
'Duration in days' => 'ระยะเวลาวันที่',
'Send email when there is no activity on a task' => 'ส่งอีเมลเมื่อไม่มีกิจกรรมเกิดขึ้นในงาน',
- 'List of external links' => 'รายการเชื่อมโยงภายนอก',
'Unable to fetch link information.' => 'ไม่สามารถดึงข้อมูลการเชื่อมโยง',
// 'Daily background job for tasks' => '',
'Auto' => 'อัตโนมัติ',
@@ -1067,9 +1056,7 @@ return array(
'External link' => 'เชื่อมโยงภายนอก',
'Copy and paste your link here...' => 'คัดลอกและวางลิงค์ของคุณที่นี้...',
'URL' => 'URL',
- 'There is no external link for the moment.' => 'ขณะนี้ไม่มีการเชื่อมโยงภายนอก',
'Internal links' => 'เชื่อมโยงภายใน',
- 'There is no internal link for the moment.' => 'ขณะนี้ไม่มีการเชื่อมโยงภายใน',
'Assign to me' => 'ฉันรับผิดชอบ',
'Me' => 'ฉัน',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => 'การจัดการผู้ใช้',
'Groups management' => 'การจัดการกลุ่ม',
'Create from another project' => 'สร้างโปรเจคอื่น',
- 'There is no subtask at the moment.' => 'ขณะนี้ไม่มีงานย่อย',
'open' => 'เปิด',
'closed' => 'ปิด',
'Priority:' => 'ความสำคัญ:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => 'เริ่ม:',
'Moved:' => 'ย้าย:',
'Task #%d' => 'งานที่ #%d',
- 'Sub-tasks' => 'งานย่อย',
'Date and time format' => 'รูปแบบของวันเวลา',
'Time format' => 'รูปแบบของเวลา',
'Start date: ' => 'เริ่มวันที่:',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 2dc611e4..f771b106 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => 'Tüm projeler',
'Add a new column' => 'Yeni sütun ekle',
'Title' => 'Başlık',
- 'Nobody assigned' => 'Kullanıcı atanmamış',
'Assigned to %s' => '%s kullanıcısına atanmış',
'Remove a column' => 'Bir sütunu sil',
'Remove a column from a board' => 'Tablodan bir sütunu sil',
@@ -166,7 +165,6 @@ return array(
'Task count' => 'Görev sayısı',
'User' => 'Kullanıcı',
'Comments' => 'Yorumlar',
- 'Write your text in Markdown' => 'Yazınızı Markdown ile yazın',
'Leave a comment' => 'Bir yorum ekle',
'Comment is required' => 'Yorum gerekli',
'Leave a description' => 'Açıklama ekleyin',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => 'Zaman takibi',
'New sub-task' => 'Yeni alt görev',
'New attachment added "%s"' => 'Yeni dosya "%s" eklendi.',
- 'Comment updated' => 'Yorum güncellendi',
'New comment posted by %s' => '%s tarafından yeni yorum eklendi',
'New attachment' => 'Yeni dosya eki',
'New comment' => 'Yeni yorum',
+ 'Comment updated' => 'Yorum güncellendi',
'New subtask' => 'Yeni alt görev',
'Subtask updated' => 'Alt görev güncellendi',
'Task updated' => 'Görev güncellendi',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formatı her zaman kabul edilir, örneğin: "%s" ve "%s"',
'New private project' => 'Yeni özel proje',
'This project is private' => 'Bu proje özel',
- 'Type here to create a new sub-task' => 'Yeni bir alt görev oluşturmak için buraya yazın',
'Add' => 'Ekle',
'Start date' => 'Başlangıç tarihi',
'Time estimated' => 'Tahmini süre',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => '"%s" için günlük proje özetinin dışa',
'Exports' => 'Dışa aktarımlar',
'This export contains the number of tasks per column grouped per day.' => 'Bu dışa aktarım günlük gruplanmış olarak her sütundaki görev sayısını içerir.',
- 'Nothing to preview...' => 'Önizleme yapılacak bir şey yok ...',
- 'Preview' => 'Önizleme',
- 'Write' => 'Değiştir',
'Active swimlanes' => 'Aktif Kulvar',
'Add a new swimlane' => 'Yeni bir Kulvar ekle',
'Change default swimlane' => 'Varsayılan Kulvarı değiştir',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => 'Görev yaşı gün olarak',
'Days in this column' => 'Bu sütunda geçirilen gün',
'%dd' => '%dG',
- 'Add a link' => 'Link ekle',
'Add a new link' => 'Yeni link ekle',
'Do you really want to remove this link: "%s"?' => '"%s" linkini gerçekten silmek istiyor musunuz?',
'Do you really want to remove this link with task #%d?' => '#%d numaralı görev ile linki gerçekten silmek istiyor musunuz?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => 'Olay akışım',
'My calendar' => 'Takvimim',
'Search tasks' => 'Görevleri ara',
- 'Back to the calendar' => 'Takvime geri dön',
- 'Filters' => 'Filtreler',
'Reset filters' => 'Filtreleri sıfırla',
'My tasks due tomorrow' => 'Yarına tamamlanması gereken görevlerim',
'Tasks due today' => 'Bugün tamamlanması gereken görevler',
@@ -850,7 +841,6 @@ return array(
'End date:' => 'Bitiş tarihi:',
'There is no start date or end date for this project.' => 'Bu proje için başlangıç veya bitiş tarihi yok.',
'Projects Gantt chart' => 'Projeler Gantt diyagramı',
- 'Link type' => 'Bağlantı türü',
'Change task color when using a specific task link' => 'Belirli bir görev bağlantısı kullanıldığında görevin rengini değiştir',
'Task link creation or modification' => 'Görev bağlantısı oluşturulması veya değiştirilmesi',
'Milestone' => 'Kilometre taşı',
@@ -902,7 +892,6 @@ return array(
'Shared' => 'Paylaşılan',
'Owner' => 'Sahibi',
'Unread notifications' => 'Okunmamış bildirimler',
- 'My filters' => 'Filtrelerim',
'Notification methods:' => 'Bildirim yöntemleri:',
'Import tasks from CSV file' => 'CSV dosyasından görevleri içeri aktar',
'Unable to read your file' => 'Dosya okunamıyor',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => 'Kullanıcı adları küçük harf ve tekil olmalı',
'Passwords will be encrypted if present' => 'Şifreler (eğer varsa) kriptolanır',
'%s attached a new file to the task %s' => '%s, %s görevine yeni dosya ekledi',
+ 'Link type' => 'Bağlantı türü',
'Assign automatically a category based on a link' => 'Bir bağlantıya göre otomatik olarak kategori ata',
'BAM - Konvertible Mark' => 'BAM - Konvertible Mark',
'Assignee Username' => 'Atanan kullanıcı adı',
@@ -1049,7 +1039,6 @@ return array(
// 'Close a task when there is no activity' => '',
// 'Duration in days' => '',
// 'Send email when there is no activity on a task' => '',
- // 'List of external links' => '',
// 'Unable to fetch link information.' => '',
// 'Daily background job for tasks' => '',
// 'Auto' => '',
@@ -1067,9 +1056,7 @@ return array(
// 'External link' => '',
// 'Copy and paste your link here...' => '',
// 'URL' => '',
- // 'There is no external link for the moment.' => '',
// 'Internal links' => '',
- // 'There is no internal link for the moment.' => '',
// 'Assign to me' => '',
// 'Me' => '',
// 'Do not duplicate anything' => '',
@@ -1077,7 +1064,6 @@ return array(
// 'Users management' => '',
// 'Groups management' => '',
// 'Create from another project' => '',
- // 'There is no subtask at the moment.' => '',
// 'open' => '',
// 'closed' => '',
// 'Priority:' => '',
@@ -1096,7 +1082,6 @@ return array(
// 'Started:' => '',
// 'Moved:' => '',
// 'Task #%d' => '',
- // 'Sub-tasks' => '',
// 'Date and time format' => '',
// 'Time format' => '',
// 'Start date: ' => '',
@@ -1137,7 +1122,6 @@ return array(
// 'User filters' => '',
// 'Category filters' => '',
// 'Upload a file' => '',
- // 'There is no attachment at the moment.' => '',
// 'View file' => '',
// 'Last activity' => '',
// 'Change subtask position' => '',
@@ -1151,4 +1135,36 @@ return array(
// 'There is no action at the moment.' => '',
// 'Import actions from another project' => '',
// 'There is no available project.' => '',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index dadedc5b..baa7693a 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -72,7 +72,6 @@ return array(
'All projects' => '所有项目',
'Add a new column' => '添加新栏目',
'Title' => '标题',
- 'Nobody assigned' => '无人被指派',
'Assigned to %s' => '指派给 %s',
'Remove a column' => '移除一个栏目',
'Remove a column from a board' => '从看板移除一个栏目',
@@ -166,7 +165,6 @@ return array(
'Task count' => '任务数',
'User' => '用户',
'Comments' => '评论',
- 'Write your text in Markdown' => '用Markdown格式编写',
'Leave a comment' => '留言',
'Comment is required' => '必须得有评论',
'Leave a description' => '给一个描述',
@@ -329,10 +327,10 @@ return array(
'Time tracking:' => '时间记录',
'New sub-task' => '新建子任务',
'New attachment added "%s"' => '新附件已添加"%s"',
- 'Comment updated' => '更新了评论',
'New comment posted by %s' => '%s 的新评论',
'New attachment' => '新建附件',
'New comment' => '新建评论',
+ 'Comment updated' => '更新了评论',
'New subtask' => '新建子任务',
'Subtask updated' => '子任务更新',
'Task updated' => '任务更新',
@@ -432,7 +430,6 @@ return array(
'ISO format is always accepted, example: "%s" and "%s"' => 'ISO 格式总是允许的,例如:"%s" 和 "%s"',
'New private project' => '新建私有项目',
'This project is private' => '此项目为私有项目',
- 'Type here to create a new sub-task' => '要创建新的子任务,请在此输入',
'Add' => '添加',
'Start date' => '启动日期',
'Time estimated' => '预计时间',
@@ -483,9 +480,6 @@ return array(
'Daily project summary export for "%s"' => '导出项目"%s"的每日汇总',
'Exports' => '导出',
'This export contains the number of tasks per column grouped per day.' => '此导出包含每列的任务数,按天分组',
- 'Nothing to preview...' => '没有需要预览的内容',
- 'Preview' => '预览',
- 'Write' => '书写',
'Active swimlanes' => '活动里程碑',
'Add a new swimlane' => '添加新里程碑',
'Change default swimlane' => '修改默认里程碑',
@@ -539,7 +533,6 @@ return array(
'Task age in days' => '任务存在天数',
'Days in this column' => '在此栏目的天数',
'%dd' => '%d天',
- 'Add a link' => '添加一个关联',
'Add a new link' => '添加一个新关联',
'Do you really want to remove this link: "%s"?' => '确认要删除此关联吗:"%s"?',
'Do you really want to remove this link with task #%d?' => '确认要删除到任务 #%d 的关联吗?',
@@ -749,8 +742,6 @@ return array(
'My activity stream' => '我的活动流',
'My calendar' => '我的日程表',
'Search tasks' => '搜索任务',
- 'Back to the calendar' => '返回日程',
- 'Filters' => '过滤器',
'Reset filters' => '重置过滤器',
'My tasks due tomorrow' => '我的明天到期的任务',
'Tasks due today' => '今天到期的任务',
@@ -850,7 +841,6 @@ return array(
'End date:' => '结束日期',
'There is no start date or end date for this project.' => '当前项目没有开始或结束日期',
'Projects Gantt chart' => '项目甘特图',
- 'Link type' => '关联类型',
'Change task color when using a specific task link' => '当任务关联到指定任务时改变颜色',
'Task link creation or modification' => '任务链接创建或更新时间',
'Milestone' => '里程碑',
@@ -902,7 +892,6 @@ return array(
'Shared' => '共享',
'Owner' => '所有人',
'Unread notifications' => '未读通知',
- 'My filters' => '我的过滤器',
'Notification methods:' => '通知提醒方式:',
'Import tasks from CSV file' => '从CSV文件导入任务',
'Unable to read your file' => '无法读取文件',
@@ -940,6 +929,7 @@ return array(
'Usernames must be lowercase and unique' => '用户名必须小写且唯一',
'Passwords will be encrypted if present' => '密码将被加密',
'%s attached a new file to the task %s' => '"%s"添加了附件到任务"%s"',
+ 'Link type' => '关联类型',
'Assign automatically a category based on a link' => '基于链接自动关联分类',
'BAM - Konvertible Mark' => '波斯尼亚马克',
'Assignee Username' => '指派用户名',
@@ -1049,7 +1039,6 @@ return array(
'Close a task when there is no activity' => '当任务没有活动记录时关闭任务',
'Duration in days' => '持续天数',
'Send email when there is no activity on a task' => '当任务没有活动记录时发送邮件',
- 'List of external links' => '列出外部关联',
'Unable to fetch link information.' => '无法获取关联信息',
'Daily background job for tasks' => '每日后台任务',
'Auto' => '自动',
@@ -1067,9 +1056,7 @@ return array(
'External link' => '外部关联',
'Copy and paste your link here...' => '复制并粘贴链接到当前位置...',
'URL' => 'URL',
- 'There is no external link for the moment.' => '当前没有外部关联。',
'Internal links' => '内部关联',
- 'There is no internal link for the moment.' => '当前没有内部关联。',
'Assign to me' => '指派给我',
'Me' => '我',
'Do not duplicate anything' => '不再重复',
@@ -1077,7 +1064,6 @@ return array(
'Users management' => '用户管理',
'Groups management' => '用户组管理',
'Create from another project' => '从另一个项目中创建',
- 'There is no subtask at the moment.' => '当前没有子任务。',
'open' => '打开',
'closed' => '已关闭',
'Priority:' => '优先级:',
@@ -1096,7 +1082,6 @@ return array(
'Started:' => '已开始:',
'Moved:' => '已移走',
'Task #%d' => '任务#%d',
- 'Sub-tasks' => '子任务',
'Date and time format' => '时间和日期格式',
'Time format' => '时间格式',
'Start date: ' => '开始时间:',
@@ -1137,7 +1122,6 @@ return array(
'User filters' => '用户过滤器',
'Category filters' => '分类过滤器',
'Upload a file' => '上传文件',
- 'There is no attachment at the moment.' => '当前没有附件。',
'View file' => '查看文件',
'Last activity' => '最后活动',
'Change subtask position' => '更改子任务位置',
@@ -1151,4 +1135,36 @@ return array(
'There is no action at the moment.' => '当前没有动作。',
'Import actions from another project' => '从另一个项目中导入动作',
'There is no available project.' => '当前没有可用项目',
+ // 'Local File' => '',
+ // 'Configuration' => '',
+ // 'PHP version:' => '',
+ // 'PHP SAPI:' => '',
+ // 'OS version:' => '',
+ // 'Database version:' => '',
+ // 'Browser:' => '',
+ // 'Task view' => '',
+ // 'Edit task' => '',
+ // 'Edit description' => '',
+ // 'New internal link' => '',
+ // 'Display list of keyboard shortcuts' => '',
+ // 'Menu' => '',
+ // 'Set start date' => '',
+ // 'Avatar' => '',
+ // 'Upload my avatar image' => '',
+ // 'Remove my image' => '',
+ // 'The OAuth2 state parameter is invalid' => '',
+ // 'User not found.' => '',
+ // 'Search in activity stream' => '',
+ // 'My activities' => '',
+ // 'Activity until yesterday' => '',
+ // 'Activity until today' => '',
+ // 'Search by creator: ' => '',
+ // 'Search by creation date: ' => '',
+ // 'Search by task status: ' => '',
+ // 'Search by task title: ' => '',
+ // 'Activity stream search' => '',
+ // 'Projects where "%s" is manager' => '',
+ // 'Projects where "%s" is member' => '',
+ // 'Open tasks assigned to "%s"' => '',
+ // 'Closed tasks assigned to "%s"' => '',
);
diff --git a/app/Model/AvatarFile.php b/app/Model/AvatarFile.php
new file mode 100644
index 00000000..c49f9fd5
--- /dev/null
+++ b/app/Model/AvatarFile.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Kanboard\Model;
+
+use Exception;
+
+/**
+ * Avatar File
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class AvatarFile extends Base
+{
+ /**
+ * Path prefix
+ *
+ * @var string
+ */
+ const PATH_PREFIX = 'avatars';
+
+ /**
+ * Get image filename
+ *
+ * @access public
+ * @param integer $user_id
+ * @return string
+ */
+ public function getFilename($user_id)
+ {
+ return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('avatar_path');
+ }
+
+ /**
+ * Add avatar in the user profile
+ *
+ * @access public
+ * @param integer $user_id Foreign key
+ * @param string $path Path on the disk
+ * @return bool
+ */
+ public function create($user_id, $path)
+ {
+ $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
+ 'avatar_path' => $path,
+ ));
+
+ $this->userSession->refresh($user_id);
+
+ return $result;
+ }
+
+ /**
+ * Remove avatar from the user profile
+ *
+ * @access public
+ * @param integer $user_id Foreign key
+ * @return bool
+ */
+ public function remove($user_id)
+ {
+ try {
+ $this->objectStorage->remove($this->getFilename($user_id));
+ $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('avatar_path' => ''));
+ $this->userSession->refresh($user_id);
+ return $result;
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Upload avatar image
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $file
+ * @return boolean
+ */
+ public function uploadFile($user_id, array $file)
+ {
+ try {
+ if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) {
+ $destination_filename = $this->generatePath($user_id, $file['name']);
+ $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename);
+ $this->create($user_id, $destination_filename);
+ } else {
+ throw new Exception('File not uploaded: '.var_export($file['error'], true));
+ }
+
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Generate the path for a new filename
+ *
+ * @access public
+ * @param integer $user_id
+ * @param string $filename
+ * @return string
+ */
+ public function generatePath($user_id, $filename)
+ {
+ return implode(DIRECTORY_SEPARATOR, array(self::PATH_PREFIX, $user_id, hash('sha1', $filename.time())));
+ }
+}
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 714b4308..a27560c8 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -31,28 +31,4 @@ abstract class Base extends \Kanboard\Core\Base
return (int) $db->getLastId();
});
}
-
- /**
- * Build SQL condition for a given time range
- *
- * @access protected
- * @param string $start_time Start timestamp
- * @param string $end_time End timestamp
- * @param string $start_column Start column name
- * @param string $end_column End column name
- * @return string
- */
- protected function getCalendarCondition($start_time, $end_time, $start_column, $end_column)
- {
- $start_column = $this->db->escapeIdentifier($start_column);
- $end_column = $this->db->escapeIdentifier($end_column);
-
- $conditions = array(
- "($start_column >= '$start_time' AND $start_column <= '$end_time')",
- "($start_column <= '$start_time' AND $end_column >= '$start_time')",
- "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))",
- );
-
- return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')';
- }
}
diff --git a/app/Model/Comment.php b/app/Model/Comment.php
index 6eb4a1e5..c5091d89 100644
--- a/app/Model/Comment.php
+++ b/app/Model/Comment.php
@@ -48,7 +48,8 @@ class Comment extends Base
self::TABLE.'.comment',
User::TABLE.'.username',
User::TABLE.'.name',
- User::TABLE.'.email'
+ User::TABLE.'.email',
+ User::TABLE.'.avatar_path'
)
->join(User::TABLE, 'id', 'user_id')
->orderBy(self::TABLE.'.date_creation', $sorting)
@@ -75,7 +76,9 @@ class Comment extends Base
self::TABLE.'.comment',
self::TABLE.'.reference',
User::TABLE.'.username',
- User::TABLE.'.name'
+ User::TABLE.'.name',
+ User::TABLE.'.email',
+ User::TABLE.'.avatar_path'
)
->join(User::TABLE, 'id', 'user_id')
->eq(self::TABLE.'.id', $comment_id)
diff --git a/app/Model/Config.php b/app/Model/Config.php
index 7b254c8d..0c363fb0 100644
--- a/app/Model/Config.php
+++ b/app/Model/Config.php
@@ -90,6 +90,7 @@ class Config extends Setting
'fi_FI' => 'Suomi',
'sv_SE' => 'Svenska',
'tr_TR' => 'Türkçe',
+ 'ko_KR' => '한국어',
'zh_CN' => '中文(简体)',
'ja_JP' => '日本語',
'th_TH' => 'ไทย',
@@ -129,6 +130,7 @@ class Config extends Setting
'fi_FI' => 'fi',
'sv_SE' => 'sv',
'tr_TR' => 'tr',
+ 'ko_KR' => 'ko',
'zh_CN' => 'zh-cn',
'ja_JP' => 'ja',
'th_TH' => 'th',
diff --git a/app/Model/File.php b/app/Model/File.php
index 03ea691d..e383235c 100644
--- a/app/Model/File.php
+++ b/app/Model/File.php
@@ -3,8 +3,8 @@
namespace Kanboard\Model;
use Exception;
+use Kanboard\Core\Thumbnail;
use Kanboard\Event\FileEvent;
-use Kanboard\Core\Tool;
use Kanboard\Core\ObjectStorage\ObjectStorageException;
/**
@@ -315,15 +315,15 @@ abstract class File extends Base
*/
public function generateThumbnailFromData($destination_filename, &$data)
{
- $temp_filename = tempnam(sys_get_temp_dir(), 'datafile');
+ $blob = Thumbnail::createFromString($data)
+ ->resize()
+ ->toString();
- file_put_contents($temp_filename, $data);
- $this->generateThumbnailFromFile($temp_filename, $destination_filename);
- unlink($temp_filename);
+ $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
}
/**
- * Generate thumbnail from a blob
+ * Generate thumbnail from a local file
*
* @access public
* @param string $uploaded_filename
@@ -331,8 +331,10 @@ abstract class File extends Base
*/
public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
{
- $thumbnail_filename = tempnam(sys_get_temp_dir(), 'thumbnail');
- Tool::generateThumbnail($uploaded_filename, $thumbnail_filename);
- $this->objectStorage->moveFile($thumbnail_filename, $this->getThumbnailPath($destination_filename));
+ $blob = Thumbnail::createFromFile($uploaded_filename)
+ ->resize()
+ ->toString();
+
+ $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
}
}
diff --git a/app/Model/Project.php b/app/Model/Project.php
index d2e5b7ce..6e3c2326 100644
--- a/app/Model/Project.php
+++ b/app/Model/Project.php
@@ -35,6 +35,20 @@ class Project extends Base
const INACTIVE = 0;
/**
+ * Value for private project
+ *
+ * @var integer
+ */
+ const TYPE_PRIVATE = 1;
+
+ /**
+ * Value for team project
+ *
+ * @var integer
+ */
+ const TYPE_TEAM = 0;
+
+ /**
* Get a project by the id
*
* @access public
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index 74df26a1..d993015b 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -2,6 +2,8 @@
namespace Kanboard\Model;
+use PicoDb\Table;
+
/**
* Project activity model
*
@@ -51,113 +53,26 @@ class ProjectActivity extends Base
}
/**
- * Get all events for the given project
- *
- * @access public
- * @param integer $project_id Project id
- * @param integer $limit Maximum events number
- * @param integer $start Timestamp of earliest activity
- * @param integer $end Timestamp of latest activity
- * @return array
- */
- public function getProject($project_id, $limit = 50, $start = null, $end = null)
- {
- return $this->getProjects(array($project_id), $limit, $start, $end);
- }
-
- /**
- * Get all events for the given projects list
+ * Get query
*
* @access public
- * @param integer[] $project_ids Projects id
- * @param integer $limit Maximum events number
- * @param integer $start Timestamp of earliest activity
- * @param integer $end Timestamp of latest activity
- * @return array
+ * @return Table
*/
- public function getProjects(array $project_ids, $limit = 50, $start = null, $end = null)
+ public function getQuery()
{
- if (empty($project_ids)) {
- return array();
- }
-
- $query = $this
- ->db
- ->table(self::TABLE)
- ->columns(
- self::TABLE.'.*',
- User::TABLE.'.username AS author_username',
- User::TABLE.'.name AS author_name',
- User::TABLE.'.email'
- )
- ->in('project_id', $project_ids)
- ->join(User::TABLE, 'id', 'creator_id')
- ->desc(self::TABLE.'.id')
- ->limit($limit);
-
- return $this->getEvents($query, $start, $end);
- }
-
- /**
- * Get all events for the given task
- *
- * @access public
- * @param integer $task_id Task id
- * @param integer $limit Maximum events number
- * @param integer $start Timestamp of earliest activity
- * @param integer $end Timestamp of latest activity
- * @return array
- */
- public function getTask($task_id, $limit = 50, $start = null, $end = null)
- {
- $query = $this
- ->db
- ->table(self::TABLE)
- ->columns(
- self::TABLE.'.*',
- User::TABLE.'.username AS author_username',
- User::TABLE.'.name AS author_name',
- User::TABLE.'.email'
- )
- ->eq('task_id', $task_id)
- ->join(User::TABLE, 'id', 'creator_id')
- ->desc(self::TABLE.'.id')
- ->limit($limit);
-
- return $this->getEvents($query, $start, $end);
- }
-
- /**
- * Common function to return events
- *
- * @access public
- * @param \PicoDb\Table $query PicoDb Query
- * @param integer $start Timestamp of earliest activity
- * @param integer $end Timestamp of latest activity
- * @return array
- */
- private function getEvents(\PicoDb\Table $query, $start, $end)
- {
- if (! is_null($start)) {
- $query->gte('date_creation', $start);
- }
-
- if (! is_null($end)) {
- $query->lte('date_creation', $end);
- }
-
- $events = $query->findAll();
-
- foreach ($events as &$event) {
- $event += $this->decode($event['data']);
- unset($event['data']);
-
- $event['author'] = $event['author_name'] ?: $event['author_username'];
- $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event);
- $event['event_content'] = $this->getContent($event);
- }
-
- return $events;
+ return $this
+ ->db
+ ->table(ProjectActivity::TABLE)
+ ->columns(
+ ProjectActivity::TABLE.'.*',
+ 'uc.username AS author_username',
+ 'uc.name AS author_name',
+ 'uc.email',
+ 'uc.avatar_path'
+ )
+ ->join(Task::TABLE, 'id', 'task_id')
+ ->join(Project::TABLE, 'id', 'project_id')
+ ->left(User::TABLE, 'uc', 'id', ProjectActivity::TABLE, 'creator_id');
}
/**
@@ -175,35 +90,4 @@ class ProjectActivity extends Base
$this->db->table(self::TABLE)->in('id', $ids)->remove();
}
}
-
- /**
- * Get the event html content
- *
- * @access public
- * @param array $params Event properties
- * @return string
- */
- public function getContent(array $params)
- {
- return $this->template->render(
- 'event/'.str_replace('.', '_', $params['event_name']),
- $params
- );
- }
-
- /**
- * Decode event data, supports unserialize() and json_decode()
- *
- * @access public
- * @param string $data Serialized data
- * @return array
- */
- public function decode($data)
- {
- if ($data{0} === 'a') {
- return unserialize($data);
- }
-
- return json_decode($data, true) ?: array();
- }
}
diff --git a/app/Model/ProjectGroupRoleFilter.php b/app/Model/ProjectGroupRoleFilter.php
deleted file mode 100644
index 989d3073..00000000
--- a/app/Model/ProjectGroupRoleFilter.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Project Group Role Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class ProjectGroupRoleFilter extends Base
-{
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @return UserFilter
- */
- public function create()
- {
- $this->query = $this->db->table(ProjectGroupRole::TABLE);
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @param string $column
- * @return array
- */
- public function findAll($column = '')
- {
- if ($column !== '') {
- return $this->query->asc($column)->findAllByColumn($column);
- }
-
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return ProjectUserRoleFilter
- */
- public function filterByProjectId($project_id)
- {
- $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id);
- return $this;
- }
-
- /**
- * Filter by username
- *
- * @access public
- * @param string $input
- * @return ProjectUserRoleFilter
- */
- public function startWithUsername($input)
- {
- $this->query
- ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
- ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
- ->ilike(User::TABLE.'.username', $input.'%');
-
- return $this;
- }
-}
diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php
index db1573ae..59af2b58 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -3,6 +3,10 @@
namespace Kanboard\Model;
use Kanboard\Core\Security\Role;
+use Kanboard\Filter\ProjectGroupRoleProjectFilter;
+use Kanboard\Filter\ProjectGroupRoleUsernameFilter;
+use Kanboard\Filter\ProjectUserRoleProjectFilter;
+use Kanboard\Filter\ProjectUserRoleUsernameFilter;
/**
* Project Permission
@@ -53,8 +57,18 @@ class ProjectPermission extends Base
*/
public function findUsernames($project_id, $input)
{
- $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
- $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
+ $userMembers = $this->projectUserRoleQuery
+ ->withFilter(new ProjectUserRoleProjectFilter($project_id))
+ ->withFilter(new ProjectUserRoleUsernameFilter($input))
+ ->getQuery()
+ ->findAllByColumn('username');
+
+ $groupMembers = $this->projectGroupRoleQuery
+ ->withFilter(new ProjectGroupRoleProjectFilter($project_id))
+ ->withFilter(new ProjectGroupRoleUsernameFilter($input))
+ ->getQuery()
+ ->findAllByColumn('username');
+
$members = array_unique(array_merge($userMembers, $groupMembers));
sort($members);
diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php
index 56da679c..2956c524 100644
--- a/app/Model/ProjectUserRole.php
+++ b/app/Model/ProjectUserRole.php
@@ -251,8 +251,8 @@ class ProjectUserRole extends Base
/**
* Copy user access from a project to another one
*
- * @param integer $project_src_id Project Template
- * @return integer $project_dst_id Project that receives the copy
+ * @param integer $project_src_id
+ * @param integer $project_dst_id
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id)
diff --git a/app/Model/ProjectUserRoleFilter.php b/app/Model/ProjectUserRoleFilter.php
deleted file mode 100644
index 64403643..00000000
--- a/app/Model/ProjectUserRoleFilter.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Project User Role Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class ProjectUserRoleFilter extends Base
-{
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @return UserFilter
- */
- public function create()
- {
- $this->query = $this->db->table(ProjectUserRole::TABLE);
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @param string $column
- * @return array
- */
- public function findAll($column = '')
- {
- if ($column !== '') {
- return $this->query->asc($column)->findAllByColumn($column);
- }
-
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return ProjectUserRoleFilter
- */
- public function filterByProjectId($project_id)
- {
- $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id);
- return $this;
- }
-
- /**
- * Filter by username
- *
- * @access public
- * @param string $input
- * @return ProjectUserRoleFilter
- */
- public function startWithUsername($input)
- {
- $this->query
- ->join(User::TABLE, 'id', 'user_id')
- ->ilike(User::TABLE.'.username', $input.'%');
-
- return $this;
- }
-}
diff --git a/app/Model/Setting.php b/app/Model/Setting.php
index f98d7ce1..c5a4765c 100644
--- a/app/Model/Setting.php
+++ b/app/Model/Setting.php
@@ -22,6 +22,7 @@ abstract class Setting extends Base
*
* @abstract
* @access public
+ * @param array $values
* @return array
*/
abstract public function prepare(array $values);
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
index b766b542..be04ee1b 100644
--- a/app/Model/SubtaskTimeTracking.php
+++ b/app/Model/SubtaskTimeTracking.php
@@ -146,94 +146,6 @@ class SubtaskTimeTracking extends Base
}
/**
- * Get user calendar events
- *
- * @access public
- * @param integer $user_id
- * @param string $start ISO-8601 format
- * @param string $end
- * @return array
- */
- public function getUserCalendarEvents($user_id, $start, $end)
- {
- $hook = 'model:subtask-time-tracking:calendar:events';
- $events = $this->getUserQuery($user_id)
- ->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'start',
- 'end'
- ))
- ->findAll();
-
- if ($this->hook->exists($hook)) {
- $events = $this->hook->first($hook, array(
- 'user_id' => $user_id,
- 'events' => $events,
- 'start' => $start,
- 'end' => $end,
- ));
- }
-
- return $this->toCalendarEvents($events);
- }
-
- /**
- * Get project calendar events
- *
- * @access public
- * @param integer $project_id
- * @param integer $start
- * @param integer $end
- * @return array
- */
- public function getProjectCalendarEvents($project_id, $start, $end)
- {
- $result = $this
- ->getProjectQuery($project_id)
- ->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'start',
- 'end'
- ))
- ->findAll();
-
- return $this->toCalendarEvents($result);
- }
-
- /**
- * Convert a record set to calendar events
- *
- * @access private
- * @param array $rows
- * @return array
- */
- private function toCalendarEvents(array $rows)
- {
- $events = array();
-
- foreach ($rows as $row) {
- $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
-
- $events[] = array(
- 'id' => $row['id'],
- 'subtask_id' => $row['subtask_id'],
- 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
- 'start' => date('Y-m-d\TH:i:s', $row['start']),
- 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
- 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
- 'borderColor' => $this->color->getBorderColor($row['color_id']),
- 'textColor' => 'black',
- 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
- 'editable' => false,
- );
- }
-
- return $events;
- }
-
- /**
* Return true if a timer is started for this use and subtask
*
* @access public
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
deleted file mode 100644
index 1883298d..00000000
--- a/app/Model/TaskFilter.php
+++ /dev/null
@@ -1,745 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Task Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class TaskFilter extends Base
-{
- /**
- * Filters mapping
- *
- * @access private
- * @var array
- */
- private $filters = array(
- 'T_ASSIGNEE' => 'filterByAssignee',
- 'T_COLOR' => 'filterByColors',
- 'T_DUE' => 'filterByDueDate',
- 'T_UPDATED' => 'filterByModificationDate',
- 'T_CREATED' => 'filterByCreationDate',
- 'T_TITLE' => 'filterByTitle',
- 'T_STATUS' => 'filterByStatusName',
- 'T_DESCRIPTION' => 'filterByDescription',
- 'T_CATEGORY' => 'filterByCategoryName',
- 'T_PROJECT' => 'filterByProjectName',
- 'T_COLUMN' => 'filterByColumnName',
- 'T_REFERENCE' => 'filterByReference',
- 'T_SWIMLANE' => 'filterBySwimlaneName',
- 'T_LINK' => 'filterByLinkName',
- );
-
- /**
- * Query
- *
- * @access public
- * @var \PicoDb\Table
- */
- public $query;
-
- /**
- * Apply filters according to the search input
- *
- * @access public
- * @param string $input
- * @return TaskFilter
- */
- public function search($input)
- {
- $tree = $this->lexer->map($this->lexer->tokenize($input));
- $this->query = $this->taskFinder->getExtendedQuery();
-
- if (empty($tree)) {
- $this->filterByTitle($input);
- }
-
- foreach ($tree as $filter => $value) {
- $method = $this->filters[$filter];
- $this->$method($value);
- }
-
- return $this;
- }
-
- /**
- * Create a new query
- *
- * @access public
- * @return TaskFilter
- */
- public function create()
- {
- $this->query = $this->db->table(Task::TABLE);
- $this->query->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id');
- $this->query->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id');
-
- $this->query->columns(
- Task::TABLE.'.*',
- 'ua.email AS assignee_email',
- 'ua.name AS assignee_name',
- 'ua.username AS assignee_username',
- 'uc.email AS creator_email',
- 'uc.username AS creator_username'
- );
-
- return $this;
- }
-
- /**
- * Create a new subtask query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function createSubtaskQuery()
- {
- return $this->db->table(Subtask::TABLE)
- ->columns(
- Subtask::TABLE.'.user_id',
- Subtask::TABLE.'.task_id',
- User::TABLE.'.name',
- User::TABLE.'.username'
- )
- ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE)
- ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE);
- }
-
- /**
- * Create a new link query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function createLinkQuery()
- {
- return $this->db->table(TaskLink::TABLE)
- ->columns(
- TaskLink::TABLE.'.task_id',
- Link::TABLE.'.label'
- )
- ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE);
- }
-
- /**
- * Clone the filter
- *
- * @access public
- * @return TaskFilter
- */
- public function copy()
- {
- $filter = new static($this->container);
- $filter->query = clone($this->query);
- $filter->query->condition = clone($this->query->condition);
- return $filter;
- }
-
- /**
- * Exclude a list of task_id
- *
- * @access public
- * @param integer[] $task_ids
- * @return TaskFilter
- */
- public function excludeTasks(array $task_ids)
- {
- $this->query->notin(Task::TABLE.'.id', $task_ids);
- return $this;
- }
-
- /**
- * Filter by id
- *
- * @access public
- * @param integer $task_id
- * @return TaskFilter
- */
- public function filterById($task_id)
- {
- if ($task_id > 0) {
- $this->query->eq(Task::TABLE.'.id', $task_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by reference
- *
- * @access public
- * @param string $reference
- * @return TaskFilter
- */
- public function filterByReference($reference)
- {
- if (! empty($reference)) {
- $this->query->eq(Task::TABLE.'.reference', $reference);
- }
-
- return $this;
- }
-
- /**
- * Filter by title
- *
- * @access public
- * @param string $title
- * @return TaskFilter
- */
- public function filterByDescription($title)
- {
- $this->query->ilike(Task::TABLE.'.description', '%'.$title.'%');
- return $this;
- }
-
- /**
- * Filter by title or id if the string is like #123 or an integer
- *
- * @access public
- * @param string $title
- * @return TaskFilter
- */
- public function filterByTitle($title)
- {
- if (ctype_digit($title) || (strlen($title) > 1 && $title{0} === '#' && ctype_digit(substr($title, 1)))) {
- $this->query->beginOr();
- $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $title));
- $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%');
- $this->query->closeOr();
- } else {
- $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%');
- }
-
- return $this;
- }
-
- /**
- * Filter by a list of project id
- *
- * @access public
- * @param array $project_ids
- * @return TaskFilter
- */
- public function filterByProjects(array $project_ids)
- {
- $this->query->in(Task::TABLE.'.project_id', $project_ids);
- return $this;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return TaskFilter
- */
- public function filterByProject($project_id)
- {
- if ($project_id > 0) {
- $this->query->eq(Task::TABLE.'.project_id', $project_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by project name
- *
- * @access public
- * @param array $values List of project name
- * @return TaskFilter
- */
- public function filterByProjectName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $project) {
- if (ctype_digit($project)) {
- $this->query->eq(Task::TABLE.'.project_id', $project);
- } else {
- $this->query->ilike(Project::TABLE.'.name', $project);
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by swimlane name
- *
- * @access public
- * @param array $values List of swimlane name
- * @return TaskFilter
- */
- public function filterBySwimlaneName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $swimlane) {
- if ($swimlane === 'default') {
- $this->query->eq(Task::TABLE.'.swimlane_id', 0);
- } else {
- $this->query->ilike(Swimlane::TABLE.'.name', $swimlane);
- $this->query->addCondition(Task::TABLE.'.swimlane_id=0 AND '.Project::TABLE.'.default_swimlane '.$this->db->getDriver()->getOperator('ILIKE')." '$swimlane'");
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by category id
- *
- * @access public
- * @param integer $category_id
- * @return TaskFilter
- */
- public function filterByCategory($category_id)
- {
- if ($category_id >= 0) {
- $this->query->eq(Task::TABLE.'.category_id', $category_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by category
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterByCategoryName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $category) {
- if ($category === 'none') {
- $this->query->eq(Task::TABLE.'.category_id', 0);
- } else {
- $this->query->eq(Category::TABLE.'.name', $category);
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by assignee
- *
- * @access public
- * @param integer $owner_id
- * @return TaskFilter
- */
- public function filterByOwner($owner_id)
- {
- if ($owner_id >= 0) {
- $this->query->eq(Task::TABLE.'.owner_id', $owner_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by assignee names
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterByAssignee(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $assignee) {
- switch ($assignee) {
- case 'me':
- $this->query->eq(Task::TABLE.'.owner_id', $this->userSession->getId());
- break;
- case 'nobody':
- $this->query->eq(Task::TABLE.'.owner_id', 0);
- break;
- default:
- $this->query->ilike(User::TABLE.'.username', '%'.$assignee.'%');
- $this->query->ilike(User::TABLE.'.name', '%'.$assignee.'%');
- }
- }
-
- $this->filterBySubtaskAssignee($values);
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by subtask assignee names
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterBySubtaskAssignee(array $values)
- {
- $subtaskQuery = $this->createSubtaskQuery();
- $subtaskQuery->beginOr();
-
- foreach ($values as $assignee) {
- if ($assignee === 'me') {
- $subtaskQuery->eq(Subtask::TABLE.'.user_id', $this->userSession->getId());
- } else {
- $subtaskQuery->ilike(User::TABLE.'.username', '%'.$assignee.'%');
- $subtaskQuery->ilike(User::TABLE.'.name', '%'.$assignee.'%');
- }
- }
-
- $subtaskQuery->closeOr();
-
- $this->query->in(Task::TABLE.'.id', $subtaskQuery->findAllByColumn('task_id'));
-
- return $this;
- }
-
- /**
- * Filter by color
- *
- * @access public
- * @param string $color_id
- * @return TaskFilter
- */
- public function filterByColor($color_id)
- {
- if ($color_id !== '') {
- $this->query->eq(Task::TABLE.'.color_id', $color_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by colors
- *
- * @access public
- * @param array $colors
- * @return TaskFilter
- */
- public function filterByColors(array $colors)
- {
- $this->query->beginOr();
-
- foreach ($colors as $color) {
- $this->filterByColor($this->color->find($color));
- }
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by column
- *
- * @access public
- * @param integer $column_id
- * @return TaskFilter
- */
- public function filterByColumn($column_id)
- {
- if ($column_id >= 0) {
- $this->query->eq(Task::TABLE.'.column_id', $column_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by column name
- *
- * @access public
- * @param array $values List of column name
- * @return TaskFilter
- */
- public function filterByColumnName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $project) {
- $this->query->ilike(Column::TABLE.'.title', $project);
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by swimlane
- *
- * @access public
- * @param integer $swimlane_id
- * @return TaskFilter
- */
- public function filterBySwimlane($swimlane_id)
- {
- if ($swimlane_id >= 0) {
- $this->query->eq(Task::TABLE.'.swimlane_id', $swimlane_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by status name
- *
- * @access public
- * @param string $status
- * @return TaskFilter
- */
- public function filterByStatusName($status)
- {
- if ($status === 'open' || $status === 'closed') {
- $this->filterByStatus($status === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED);
- }
-
- return $this;
- }
-
- /**
- * Filter by status
- *
- * @access public
- * @param integer $is_active
- * @return TaskFilter
- */
- public function filterByStatus($is_active)
- {
- if ($is_active >= 0) {
- $this->query->eq(Task::TABLE.'.is_active', $is_active);
- }
-
- return $this;
- }
-
- /**
- * Filter by link
- *
- * @access public
- * @param array $values List of links
- * @return TaskFilter
- */
- public function filterByLinkName(array $values)
- {
- $this->query->beginOr();
-
- $link_query = $this->createLinkQuery()->in(Link::TABLE.'.label', $values);
- $matching_task_ids = $link_query->findAllByColumn('task_id');
- if (empty($matching_task_ids)) {
- $this->query->eq(Task::TABLE.'.id', 0);
- } else {
- $this->query->in(Task::TABLE.'.id', $matching_task_ids);
- }
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by due date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByDueDate($date)
- {
- $this->query->neq(Task::TABLE.'.date_due', 0);
- $this->query->notNull(Task::TABLE.'.date_due');
- return $this->filterWithOperator(Task::TABLE.'.date_due', $date, true);
- }
-
- /**
- * Filter by due date (range)
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByDueDateRange($start, $end)
- {
- $this->query->gte('date_due', $this->dateParser->getTimestampFromIsoFormat($start));
- $this->query->lte('date_due', $this->dateParser->getTimestampFromIsoFormat($end));
-
- return $this;
- }
-
- /**
- * Filter by start date (range)
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByStartDateRange($start, $end)
- {
- $this->query->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'date_started',
- 'date_completed'
- ));
-
- return $this;
- }
-
- /**
- * Filter by creation date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByCreationDate($date)
- {
- if ($date === 'recently') {
- return $this->filterRecentlyDate(Task::TABLE.'.date_creation');
- }
-
- return $this->filterWithOperator(Task::TABLE.'.date_creation', $date, true);
- }
-
- /**
- * Filter by creation date
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByCreationDateRange($start, $end)
- {
- $this->query->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'date_creation',
- 'date_completed'
- ));
-
- return $this;
- }
-
- /**
- * Filter by modification date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByModificationDate($date)
- {
- if ($date === 'recently') {
- return $this->filterRecentlyDate(Task::TABLE.'.date_modification');
- }
-
- return $this->filterWithOperator(Task::TABLE.'.date_modification', $date, true);
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @return array
- */
- public function findAll()
- {
- return $this->query->asc(Task::TABLE.'.id')->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Get swimlanes and tasks to display the board
- *
- * @access public
- * @return array
- */
- public function getBoard($project_id)
- {
- $tasks = $this->filterByProject($project_id)->query->asc(Task::TABLE.'.position')->findAll();
-
- return $this->board->getBoard($project_id, function ($project_id, $column_id, $swimlane_id) use ($tasks) {
- return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) {
- return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id;
- });
- });
- }
-
- /**
- * Filter with an operator
- *
- * @access public
- * @param string $field
- * @param string $value
- * @param boolean $is_date
- * @return TaskFilter
- */
- private function filterWithOperator($field, $value, $is_date)
- {
- $operators = array(
- '<=' => 'lte',
- '>=' => 'gte',
- '<' => 'lt',
- '>' => 'gt',
- );
-
- foreach ($operators as $operator => $method) {
- if (strpos($value, $operator) === 0) {
- $value = substr($value, strlen($operator));
- $this->query->$method($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
- return $this;
- }
- }
-
- if ($is_date) {
- $timestamp = $this->dateParser->getTimestampFromIsoFormat($value);
- $this->query->gte($field, $timestamp);
- $this->query->lte($field, $timestamp + 86399);
- } else {
- $this->query->eq($field, $value);
- }
-
- return $this;
- }
-
- /**
- * Use the board_highlight_period for the "recently" keyword
- *
- * @access private
- * @param string $field
- * @return TaskFilter
- */
- private function filterRecentlyDate($field)
- {
- $duration = $this->config->get('board_highlight_period', 0);
-
- if ($duration > 0) {
- $this->query->gte($field, time() - $duration);
- }
-
- return $this;
- }
-}
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index d67372cc..d406b794 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -128,6 +128,7 @@ class TaskFinder extends Base
User::TABLE.'.username AS assignee_username',
User::TABLE.'.name AS assignee_name',
User::TABLE.'.email AS assignee_email',
+ User::TABLE.'.avatar_path AS assignee_avatar_path',
Category::TABLE.'.name AS category_name',
Category::TABLE.'.description AS category_description',
Column::TABLE.'.title AS column_name',
@@ -137,6 +138,7 @@ class TaskFinder extends Base
Project::TABLE.'.name AS project_name'
)
->join(User::TABLE, 'id', 'owner_id', Task::TABLE)
+ ->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id')
->join(Category::TABLE, 'id', 'category_id', Task::TABLE)
->join(Column::TABLE, 'id', 'column_id', Task::TABLE)
->join(Swimlane::TABLE, 'id', 'swimlane_id', Task::TABLE)
@@ -362,6 +364,27 @@ class TaskFinder extends Base
}
/**
+ * Get iCal query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getICalQuery()
+ {
+ return $this->db->table(Task::TABLE)
+ ->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id')
+ ->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id')
+ ->columns(
+ Task::TABLE.'.*',
+ 'ua.email AS assignee_email',
+ 'ua.name AS assignee_name',
+ 'ua.username AS assignee_username',
+ 'uc.email AS creator_email',
+ 'uc.username AS creator_username'
+ );
+ }
+
+ /**
* Count all tasks for a given project and status
*
* @access public
diff --git a/app/Model/User.php b/app/Model/User.php
index 0e11422b..57993002 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -283,12 +283,7 @@ class User extends Base
{
$this->prepare($values);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
-
- // If the user is connected refresh his session
- if ($this->userSession->getId() == $values['id']) {
- $this->userSession->initialize($this->getById($this->userSession->getId()));
- }
-
+ $this->userSession->refresh($values['id']);
return $result;
}
@@ -325,6 +320,8 @@ class User extends Base
*/
public function remove($user_id)
{
+ $this->avatarFile->remove($user_id);
+
return $this->db->transaction(function (Database $db) use ($user_id) {
// All assigned tasks are now unassigned (no foreign key)
diff --git a/app/Model/UserFilter.php b/app/Model/UserFilter.php
deleted file mode 100644
index ff546e96..00000000
--- a/app/Model/UserFilter.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * User Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class UserFilter extends Base
-{
- /**
- * Search query
- *
- * @access private
- * @var string
- */
- private $input;
-
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @param string $input
- * @return UserFilter
- */
- public function create($input)
- {
- $this->query = $this->db->table(User::TABLE);
- $this->input = $input;
- return $this;
- }
-
- /**
- * Filter users by name or username
- *
- * @access public
- * @return UserFilter
- */
- public function filterByUsernameOrByName()
- {
- $this->query->beginOr()
- ->ilike('username', '%'.$this->input.'%')
- ->ilike('name', '%'.$this->input.'%')
- ->closeOr();
-
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @return array
- */
- public function findAll()
- {
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-}
diff --git a/app/Model/UserNotification.php b/app/Model/UserNotification.php
index e8a967ac..7795da2e 100644
--- a/app/Model/UserNotification.php
+++ b/app/Model/UserNotification.php
@@ -117,23 +117,20 @@ class UserNotification extends Base
*/
public function saveSettings($user_id, array $values)
{
- $this->db->startTransaction();
+ $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']);
- if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) {
+ if (! empty($types)) {
$this->enableNotification($user_id);
-
- $filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter'];
- $projects = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']);
- $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']);
-
- $this->userNotificationFilter->saveFilter($user_id, $filter);
- $this->userNotificationFilter->saveSelectedProjects($user_id, $projects);
- $this->userNotificationType->saveSelectedTypes($user_id, $types);
} else {
$this->disableNotification($user_id);
}
- $this->db->closeTransaction();
+ $filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter'];
+ $project_ids = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']);
+
+ $this->userNotificationFilter->saveFilter($user_id, $filter);
+ $this->userNotificationFilter->saveSelectedProjects($user_id, $project_ids);
+ $this->userNotificationType->saveSelectedTypes($user_id, $types);
}
/**
diff --git a/app/Model/UserNotificationFilter.php b/app/Model/UserNotificationFilter.php
index d4afd278..780ddfc7 100644
--- a/app/Model/UserNotificationFilter.php
+++ b/app/Model/UserNotificationFilter.php
@@ -61,10 +61,11 @@ class UserNotificationFilter extends Base
* @access public
* @param integer $user_id
* @param string $filter
+ * @return boolean
*/
public function saveFilter($user_id, $filter)
{
- $this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
+ return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array(
'notifications_filter' => $filter,
));
}
@@ -87,17 +88,21 @@ class UserNotificationFilter extends Base
* @access public
* @param integer $user_id
* @param integer[] $project_ids
+ * @return boolean
*/
public function saveSelectedProjects($user_id, array $project_ids)
{
+ $results = array();
$this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove();
foreach ($project_ids as $project_id) {
- $this->db->table(self::PROJECT_TABLE)->insert(array(
+ $results[] = $this->db->table(self::PROJECT_TABLE)->insert(array(
'user_id' => $user_id,
'project_id' => $project_id,
));
}
+
+ return !in_array(false, $results, true);
}
/**
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 9bfe6649..934b063f 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,23 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 108;
+const VERSION = 110;
+
+function version_110(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE user_has_notifications DROP FOREIGN KEY `user_has_notifications_ibfk_1`");
+ $pdo->exec("ALTER TABLE user_has_notifications DROP FOREIGN KEY `user_has_notifications_ibfk_2`");
+ $pdo->exec("DROP INDEX `project_id` ON user_has_notifications");
+ $pdo->exec("ALTER TABLE user_has_notifications DROP KEY `user_id`");
+ $pdo->exec("CREATE UNIQUE INDEX `user_has_notifications_unique_idx` ON `user_has_notifications` (`user_id`, `project_id`)");
+ $pdo->exec("ALTER TABLE user_has_notifications ADD CONSTRAINT user_has_notifications_ibfk_1 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE");
+ $pdo->exec("ALTER TABLE user_has_notifications ADD CONSTRAINT user_has_notifications_ibfk_2 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE");
+}
+
+function version_109(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)");
+}
function version_108(PDO $pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 28c563de..3ef49498 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,12 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
-const VERSION = 88;
+const VERSION = 89;
+
+function version_89(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)");
+}
function version_88(PDO $pdo)
{
diff --git a/app/Schema/Sql/mysql.sql b/app/Schema/Sql/mysql.sql
index b5620400..ce2374f0 100644
--- a/app/Schema/Sql/mysql.sql
+++ b/app/Schema/Sql/mysql.sql
@@ -273,6 +273,8 @@ CREATE TABLE `project_has_metadata` (
`project_id` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
`value` varchar(255) DEFAULT '',
+ `changed_by` int(11) NOT NULL DEFAULT '0',
+ `changed_on` int(11) NOT NULL DEFAULT '0',
UNIQUE KEY `project_id` (`project_id`,`name`),
CONSTRAINT `project_has_metadata_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -357,6 +359,8 @@ DROP TABLE IF EXISTS `settings`;
CREATE TABLE `settings` (
`option` varchar(100) NOT NULL,
`value` varchar(255) DEFAULT '',
+ `changed_by` int(11) NOT NULL DEFAULT '0',
+ `changed_on` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`option`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -469,6 +473,8 @@ CREATE TABLE `task_has_metadata` (
`task_id` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
`value` varchar(255) DEFAULT '',
+ `changed_by` int(11) NOT NULL DEFAULT '0',
+ `changed_on` int(11) NOT NULL DEFAULT '0',
UNIQUE KEY `task_id` (`task_id`,`name`),
CONSTRAINT `task_has_metadata_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -548,6 +554,8 @@ CREATE TABLE `user_has_metadata` (
`user_id` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
`value` varchar(255) DEFAULT '',
+ `changed_by` int(11) NOT NULL DEFAULT '0',
+ `changed_on` int(11) NOT NULL DEFAULT '0',
UNIQUE KEY `user_id` (`user_id`,`name`),
CONSTRAINT `user_has_metadata_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -570,8 +578,8 @@ DROP TABLE IF EXISTS `user_has_notifications`;
CREATE TABLE `user_has_notifications` (
`user_id` int(11) NOT NULL,
`project_id` int(11) NOT NULL,
- UNIQUE KEY `project_id` (`project_id`,`user_id`),
- KEY `user_id` (`user_id`),
+ UNIQUE KEY `user_has_notifications_unique_idx` (`user_id`,`project_id`),
+ KEY `user_has_notifications_ibfk_2` (`project_id`),
CONSTRAINT `user_has_notifications_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `user_has_notifications_ibfk_2` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -615,6 +623,7 @@ CREATE TABLE `users` (
`gitlab_id` int(11) DEFAULT NULL,
`role` varchar(25) NOT NULL DEFAULT 'app-user',
`is_active` tinyint(1) DEFAULT '1',
+ `avatar_path` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_username_idx` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@@ -638,7 +647,7 @@ CREATE TABLE `users` (
LOCK TABLES `settings` WRITE;
/*!40000 ALTER TABLE `settings` DISABLE KEYS */;
-INSERT INTO `settings` VALUES ('api_token','cd9c46c6bdaa6afc49b3385dabe0b78c059bc124b1f72c2f47c9ca604cf1'),('application_currency','USD'),('application_date_format','m/d/Y'),('application_language','en_US'),('application_stylesheet',''),('application_timezone','UTC'),('application_url',''),('board_columns',''),('board_highlight_period','172800'),('board_private_refresh_interval','10'),('board_public_refresh_interval','60'),('calendar_project_tasks','date_started'),('calendar_user_subtasks_time_tracking','0'),('calendar_user_tasks','date_started'),('cfd_include_closed_tasks','1'),('default_color','yellow'),('integration_gravatar','0'),('password_reset','1'),('project_categories',''),('subtask_restriction','0'),('subtask_time_tracking','1'),('webhook_token','32387b121de8fe6031a6b71b7b1b9cae411a909539aa9d494cf69ac5f2ee'),('webhook_url','');
+INSERT INTO `settings` VALUES ('api_token','9c55053ae1d523893efc820e2e8338c4cf47f5c6c2c26861fec637eba62b',0,0),('application_currency','USD',0,0),('application_date_format','m/d/Y',0,0),('application_language','en_US',0,0),('application_stylesheet','',0,0),('application_timezone','UTC',0,0),('application_url','',0,0),('board_columns','',0,0),('board_highlight_period','172800',0,0),('board_private_refresh_interval','10',0,0),('board_public_refresh_interval','60',0,0),('calendar_project_tasks','date_started',0,0),('calendar_user_subtasks_time_tracking','0',0,0),('calendar_user_tasks','date_started',0,0),('cfd_include_closed_tasks','1',0,0),('default_color','yellow',0,0),('integration_gravatar','0',0,0),('password_reset','1',0,0),('project_categories','',0,0),('subtask_restriction','0',0,0),('subtask_time_tracking','1',0,0),('webhook_token','aaed762f4f6b0860902af0e2a87e5ad3427d24ff9e3ce8a2e0b005b58dfc',0,0),('webhook_url','',0,0);
/*!40000 ALTER TABLE `settings` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -667,4 +676,4 @@ UNLOCK TABLES;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$tyByY1dfUO9S.2wpJcSMEO4UU9H.yCwf/pmzo430DM2C4QZ/K3Kt2', 'app-admin');INSERT INTO schema_version VALUES ('107');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$e.SftITKuBvXeNbxtmTKS.KAbIy4Mx09t254BAiEAuWOxkuS4xfLG', 'app-admin');INSERT INTO schema_version VALUES ('110');
diff --git a/app/Schema/Sql/postgres.sql b/app/Schema/Sql/postgres.sql
index c613ddb4..48a269d3 100644
--- a/app/Schema/Sql/postgres.sql
+++ b/app/Schema/Sql/postgres.sql
@@ -512,7 +512,9 @@ CREATE TABLE project_has_groups (
CREATE TABLE project_has_metadata (
project_id integer NOT NULL,
name character varying(50) NOT NULL,
- value character varying(255) DEFAULT ''::character varying
+ value character varying(255) DEFAULT ''::character varying,
+ changed_by integer DEFAULT 0 NOT NULL,
+ changed_on integer DEFAULT 0 NOT NULL
);
@@ -652,7 +654,9 @@ CREATE TABLE schema_version (
CREATE TABLE settings (
option character varying(100) NOT NULL,
- value character varying(255) DEFAULT ''::character varying
+ value character varying(255) DEFAULT ''::character varying,
+ changed_by integer DEFAULT 0 NOT NULL,
+ changed_on integer DEFAULT 0 NOT NULL
);
@@ -847,7 +851,9 @@ ALTER SEQUENCE task_has_links_id_seq OWNED BY task_has_links.id;
CREATE TABLE task_has_metadata (
task_id integer NOT NULL,
name character varying(50) NOT NULL,
- value character varying(255) DEFAULT ''::character varying
+ value character varying(255) DEFAULT ''::character varying,
+ changed_by integer DEFAULT 0 NOT NULL,
+ changed_on integer DEFAULT 0 NOT NULL
);
@@ -969,7 +975,9 @@ ALTER SEQUENCE transitions_id_seq OWNED BY transitions.id;
CREATE TABLE user_has_metadata (
user_id integer NOT NULL,
name character varying(50) NOT NULL,
- value character varying(255) DEFAULT ''::character varying
+ value character varying(255) DEFAULT ''::character varying,
+ changed_by integer DEFAULT 0 NOT NULL,
+ changed_on integer DEFAULT 0 NOT NULL
);
@@ -1070,7 +1078,8 @@ CREATE TABLE users (
lock_expiration_date bigint DEFAULT 0,
gitlab_id integer,
role character varying(25) DEFAULT 'app-user'::character varying NOT NULL,
- is_active boolean DEFAULT true
+ is_active boolean DEFAULT true,
+ avatar_path character varying(255)
);
@@ -2141,29 +2150,29 @@ SET search_path = public, pg_catalog;
-- Data for Name: settings; Type: TABLE DATA; Schema: public; Owner: postgres
--
-INSERT INTO settings (option, value) VALUES ('board_highlight_period', '172800');
-INSERT INTO settings (option, value) VALUES ('board_public_refresh_interval', '60');
-INSERT INTO settings (option, value) VALUES ('board_private_refresh_interval', '10');
-INSERT INTO settings (option, value) VALUES ('board_columns', '');
-INSERT INTO settings (option, value) VALUES ('webhook_token', 'c7caaf8f87ad391800e3989d7abfd98a6066a6f801fc151012bb5c4ee3cb');
-INSERT INTO settings (option, value) VALUES ('api_token', 'b0a6f56fe236fc9639fc6914e92365aa627d95cd790aa7e0c5a3ebebf844');
-INSERT INTO settings (option, value) VALUES ('application_language', 'en_US');
-INSERT INTO settings (option, value) VALUES ('application_timezone', 'UTC');
-INSERT INTO settings (option, value) VALUES ('application_url', '');
-INSERT INTO settings (option, value) VALUES ('application_date_format', 'm/d/Y');
-INSERT INTO settings (option, value) VALUES ('project_categories', '');
-INSERT INTO settings (option, value) VALUES ('subtask_restriction', '0');
-INSERT INTO settings (option, value) VALUES ('application_stylesheet', '');
-INSERT INTO settings (option, value) VALUES ('application_currency', 'USD');
-INSERT INTO settings (option, value) VALUES ('integration_gravatar', '0');
-INSERT INTO settings (option, value) VALUES ('calendar_user_subtasks_time_tracking', '0');
-INSERT INTO settings (option, value) VALUES ('calendar_user_tasks', 'date_started');
-INSERT INTO settings (option, value) VALUES ('calendar_project_tasks', 'date_started');
-INSERT INTO settings (option, value) VALUES ('webhook_url', '');
-INSERT INTO settings (option, value) VALUES ('default_color', 'yellow');
-INSERT INTO settings (option, value) VALUES ('subtask_time_tracking', '1');
-INSERT INTO settings (option, value) VALUES ('cfd_include_closed_tasks', '1');
-INSERT INTO settings (option, value) VALUES ('password_reset', '1');
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_highlight_period', '172800', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_public_refresh_interval', '60', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_private_refresh_interval', '10', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('board_columns', '', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_token', '67545fef6a0a3f43d60c7d57632d6e4af9930f064c12e72266b1c9b42381', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('api_token', 'c16b1c5896b258409a5eb344152b5b33c8ef4c58902bc543fc1348c37975', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_language', 'en_US', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_timezone', 'UTC', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_url', '', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_date_format', 'm/d/Y', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('project_categories', '', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('subtask_restriction', '0', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_stylesheet', '', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('application_currency', 'USD', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('integration_gravatar', '0', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_user_subtasks_time_tracking', '0', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_user_tasks', 'date_started', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('calendar_project_tasks', 'date_started', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('webhook_url', '', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('default_color', 'yellow', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('subtask_time_tracking', '1', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('cfd_include_closed_tasks', '1', 0, 0);
+INSERT INTO settings (option, value, changed_by, changed_on) VALUES ('password_reset', '1', 0, 0);
--
@@ -2211,4 +2220,4 @@ SELECT pg_catalog.setval('links_id_seq', 11, true);
-- PostgreSQL database dump complete
--
-INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$tyByY1dfUO9S.2wpJcSMEO4UU9H.yCwf/pmzo430DM2C4QZ/K3Kt2', 'app-admin');INSERT INTO schema_version VALUES ('87');
+INSERT INTO users (username, password, role) VALUES ('admin', '$2y$10$e.SftITKuBvXeNbxtmTKS.KAbIy4Mx09t254BAiEAuWOxkuS4xfLG', 'app-admin');INSERT INTO schema_version VALUES ('89');
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index c6f60332..9ded7ed9 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use PDO;
-const VERSION = 100;
+const VERSION = 101;
+
+function version_101(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path TEXT");
+}
function version_100(PDO $pdo)
{
diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php
index d59ffd9e..776e65d5 100644
--- a/app/ServiceProvider/AuthenticationProvider.php
+++ b/app/ServiceProvider/AuthenticationProvider.php
@@ -125,6 +125,7 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->add('Board', 'readonly', Role::APP_PUBLIC);
$acl->add('Ical', '*', Role::APP_PUBLIC);
$acl->add('Feed', '*', Role::APP_PUBLIC);
+ $acl->add('AvatarFile', 'show', Role::APP_PUBLIC);
$acl->add('Config', '*', Role::APP_ADMIN);
$acl->add('Currency', '*', Role::APP_ADMIN);
diff --git a/app/ServiceProvider/AvatarProvider.php b/app/ServiceProvider/AvatarProvider.php
index 73d37d5c..aac4fcab 100644
--- a/app/ServiceProvider/AvatarProvider.php
+++ b/app/ServiceProvider/AvatarProvider.php
@@ -6,6 +6,7 @@ use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Kanboard\Core\User\Avatar\AvatarManager;
use Kanboard\User\Avatar\GravatarProvider;
+use Kanboard\User\Avatar\AvatarFileProvider;
use Kanboard\User\Avatar\LetterAvatarProvider;
/**
@@ -28,6 +29,7 @@ class AvatarProvider implements ServiceProviderInterface
$container['avatarManager'] = new AvatarManager;
$container['avatarManager']->register(new LetterAvatarProvider($container));
$container['avatarManager']->register(new GravatarProvider($container));
+ $container['avatarManager']->register(new AvatarFileProvider($container));
return $container;
}
}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index b883c905..18c1d578 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface
'Model' => array(
'Action',
'ActionParameter',
+ 'AvatarFile',
'Board',
'Category',
'Color',
@@ -48,9 +49,7 @@ class ClassProvider implements ServiceProviderInterface
'ProjectNotification',
'ProjectMetadata',
'ProjectGroupRole',
- 'ProjectGroupRoleFilter',
'ProjectUserRole',
- 'ProjectUserRoleFilter',
'RememberMeSession',
'Subtask',
'SubtaskTimeTracking',
@@ -62,7 +61,6 @@ class ClassProvider implements ServiceProviderInterface
'TaskExternalLink',
'TaskFinder',
'TaskFile',
- 'TaskFilter',
'TaskLink',
'TaskModification',
'TaskPermission',
@@ -78,15 +76,6 @@ class ClassProvider implements ServiceProviderInterface
'UserUnreadNotification',
'UserMetadata',
),
- 'Formatter' => array(
- 'TaskFilterGanttFormatter',
- 'TaskFilterAutoCompleteFormatter',
- 'TaskFilterCalendarFormatter',
- 'TaskFilterICalendarFormatter',
- 'ProjectGanttFormatter',
- 'UserFilterAutoCompleteFormatter',
- 'GroupAutoCompleteFormatter',
- ),
'Validator' => array(
'ActionValidator',
'AuthValidator',
diff --git a/app/ServiceProvider/DatabaseProvider.php b/app/ServiceProvider/DatabaseProvider.php
index 8cede8af..d323807d 100644
--- a/app/ServiceProvider/DatabaseProvider.php
+++ b/app/ServiceProvider/DatabaseProvider.php
@@ -44,8 +44,8 @@ class DatabaseProvider implements ServiceProviderInterface
if ($db->schema()->check(\Schema\VERSION)) {
return $db;
} else {
- $errors = $db->getLogMessages();
- throw new RuntimeException('Unable to migrate database schema: '.(isset($errors[0]) ? $errors[0] : 'Unknown error'));
+ $messages = $db->getLogMessages();
+ throw new RuntimeException('Unable to run SQL migrations: '.implode(', ', $messages).' (You may have to fix it manually)');
}
}
diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php
new file mode 100644
index 00000000..f3918d77
--- /dev/null
+++ b/app/ServiceProvider/FilterProvider.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Kanboard\Core\Filter\LexerBuilder;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\ProjectActivityCreationDateFilter;
+use Kanboard\Filter\ProjectActivityCreatorFilter;
+use Kanboard\Filter\ProjectActivityProjectNameFilter;
+use Kanboard\Filter\ProjectActivityTaskStatusFilter;
+use Kanboard\Filter\ProjectActivityTaskTitleFilter;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskCategoryFilter;
+use Kanboard\Filter\TaskColorFilter;
+use Kanboard\Filter\TaskColumnFilter;
+use Kanboard\Filter\TaskCommentFilter;
+use Kanboard\Filter\TaskCreationDateFilter;
+use Kanboard\Filter\TaskCreatorFilter;
+use Kanboard\Filter\TaskDescriptionFilter;
+use Kanboard\Filter\TaskDueDateFilter;
+use Kanboard\Filter\TaskIdFilter;
+use Kanboard\Filter\TaskLinkFilter;
+use Kanboard\Filter\TaskModificationDateFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskReferenceFilter;
+use Kanboard\Filter\TaskStatusFilter;
+use Kanboard\Filter\TaskSubtaskAssigneeFilter;
+use Kanboard\Filter\TaskSwimlaneFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Model\ProjectUserRole;
+use Kanboard\Model\User;
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Filter Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class FilterProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $this->createUserFilter($container);
+ $this->createProjectFilter($container);
+ $this->createTaskFilter($container);
+ return $container;
+ }
+
+ public function createUserFilter(Container $container)
+ {
+ $container['userQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(User::TABLE));
+ return $builder;
+ });
+
+ return $container;
+ }
+
+ public function createProjectFilter(Container $container)
+ {
+ $container['projectGroupRoleQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(ProjectGroupRole::TABLE));
+ return $builder;
+ });
+
+ $container['projectUserRoleQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(ProjectUserRole::TABLE));
+ return $builder;
+ });
+
+ $container['projectQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(Project::TABLE));
+ return $builder;
+ });
+
+ $container['projectActivityLexer'] = $container->factory(function ($c) {
+ $builder = new LexerBuilder();
+ $builder
+ ->withQuery($c['projectActivity']->getQuery())
+ ->withFilter(new ProjectActivityTaskTitleFilter(), true)
+ ->withFilter(new ProjectActivityTaskStatusFilter())
+ ->withFilter(new ProjectActivityProjectNameFilter())
+ ->withFilter(ProjectActivityCreationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(ProjectActivityCreatorFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ )
+ ;
+
+ return $builder;
+ });
+
+ $container['projectActivityQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['projectActivity']->getQuery());
+
+ return $builder;
+ });
+
+ return $container;
+ }
+
+ public function createTaskFilter(Container $container)
+ {
+ $container['taskQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['taskFinder']->getExtendedQuery());
+ return $builder;
+ });
+
+ $container['taskLexer'] = $container->factory(function ($c) {
+ $builder = new LexerBuilder();
+
+ $builder
+ ->withQuery($c['taskFinder']->getExtendedQuery())
+ ->withFilter(TaskAssigneeFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ )
+ ->withFilter(new TaskCategoryFilter())
+ ->withFilter(TaskColorFilter::getInstance()
+ ->setColorModel($c['color'])
+ )
+ ->withFilter(new TaskColumnFilter())
+ ->withFilter(new TaskCommentFilter())
+ ->withFilter(TaskCreationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(TaskCreatorFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ )
+ ->withFilter(new TaskDescriptionFilter())
+ ->withFilter(TaskDueDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(new TaskIdFilter())
+ ->withFilter(TaskLinkFilter::getInstance()
+ ->setDatabase($c['db'])
+ )
+ ->withFilter(TaskModificationDateFilter::getInstance()
+ ->setDateParser($c['dateParser'])
+ )
+ ->withFilter(new TaskProjectFilter())
+ ->withFilter(new TaskReferenceFilter())
+ ->withFilter(new TaskStatusFilter())
+ ->withFilter(TaskSubtaskAssigneeFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ ->setDatabase($c['db'])
+ )
+ ->withFilter(new TaskSwimlaneFilter())
+ ->withFilter(new TaskTitleFilter(), true)
+ ;
+
+ return $builder;
+ });
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/HelperProvider.php b/app/ServiceProvider/HelperProvider.php
index 37be5a05..bf3956a2 100644
--- a/app/ServiceProvider/HelperProvider.php
+++ b/app/ServiceProvider/HelperProvider.php
@@ -13,12 +13,14 @@ class HelperProvider implements ServiceProviderInterface
{
$container['helper'] = new Helper($container);
$container['helper']->register('app', '\Kanboard\Helper\AppHelper');
+ $container['helper']->register('calendar', '\Kanboard\Helper\CalendarHelper');
$container['helper']->register('asset', '\Kanboard\Helper\AssetHelper');
$container['helper']->register('board', '\Kanboard\Helper\BoardHelper');
$container['helper']->register('dt', '\Kanboard\Helper\DateHelper');
$container['helper']->register('file', '\Kanboard\Helper\FileHelper');
$container['helper']->register('form', '\Kanboard\Helper\FormHelper');
$container['helper']->register('hook', '\Kanboard\Helper\HookHelper');
+ $container['helper']->register('ical', '\Kanboard\Helper\ICalHelper');
$container['helper']->register('layout', '\Kanboard\Helper\LayoutHelper');
$container['helper']->register('model', '\Kanboard\Helper\ModelHelper');
$container['helper']->register('subtask', '\Kanboard\Helper\SubtaskHelper');
@@ -27,6 +29,8 @@ class HelperProvider implements ServiceProviderInterface
$container['helper']->register('url', '\Kanboard\Helper\UrlHelper');
$container['helper']->register('user', '\Kanboard\Helper\UserHelper');
$container['helper']->register('avatar', '\Kanboard\Helper\AvatarHelper');
+ $container['helper']->register('projectHeader', '\Kanboard\Helper\ProjectHeaderHelper');
+ $container['helper']->register('projectActivity', '\Kanboard\Helper\ProjectActivityHelper');
$container['template'] = new Template($container['helper']);
diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php
index 0e7548d4..30d23a51 100644
--- a/app/ServiceProvider/RouteProvider.php
+++ b/app/ServiceProvider/RouteProvider.php
@@ -42,7 +42,7 @@ class RouteProvider implements ServiceProviderInterface
// Search routes
$container['route']->addRoute('search', 'search', 'index');
- $container['route']->addRoute('search/:search', 'search', 'index');
+ $container['route']->addRoute('search/activity', 'search', 'activity');
// ProjectCreation routes
$container['route']->addRoute('project/create', 'ProjectCreation', 'create');
@@ -62,6 +62,7 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('project/:project_id/enable', 'project', 'enable');
$container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index');
$container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1');
+ $container['route']->addRoute('project/:project_id/activity', 'activity', 'project');
// Project Overview
$container['route']->addRoute('project/:project_id/overview', 'ProjectOverview', 'show');
diff --git a/app/Template/activity/filter_dropdown.php b/app/Template/activity/filter_dropdown.php
new file mode 100644
index 00000000..8d7a7de3
--- /dev/null
+++ b/app/Template/activity/filter_dropdown.php
@@ -0,0 +1,14 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Default filters') ?>"><i class="fa fa-filter fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li><a href="#" class="filter-helper filter-reset" data-filter="" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="creator:me"><?= t('My activities') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d', strtotime('yesterday')) ?>"><?= t('Activity until yesterday') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d')?>"><?= t('Activity until today') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="status:closed"><?= t('Closed tasks') ?></a></li>
+ <li><a href="#" class="filter-helper" data-filter="status:open"><?= t('Open tasks') ?></a></li>
+ <li>
+ <?= $this->url->doc(t('View advanced search syntax'), 'search') ?>
+ </li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Template/activity/project.php b/app/Template/activity/project.php
index ba6d6629..176d9b99 100644
--- a/app/Template/activity/project.php
+++ b/app/Template/activity/project.php
@@ -1,40 +1,14 @@
<section id="main">
- <div class="page-header">
+ <?= $this->projectHeader->render($project, 'Analytic', $this->app->getRouterAction()) ?>
+
+ <?php if ($project['is_public']): ?>
+ <div class="menu-inline pull-right">
<ul>
- <li>
- <span class="dropdown">
- <span>
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?= $this->render('project/dropdown', array('project' => $project)) ?>
- </ul>
- </span>
- </span>
- </li>
- <li>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <li>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?>
- <li>
- <i class="fa fa-cog fa-fw"></i>
- <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php endif ?>
- <li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
- </li>
- <?php if ($project['is_public']): ?>
- <li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li>
- <li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li>
- <?php endif ?>
+ <li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'feed', 'project', array('token' => $project['token']), false, '', '', true) ?></li>
+ <li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li>
</ul>
</div>
+ <?php endif ?>
<?= $this->render('event/events', array('events' => $events)) ?>
</section> \ No newline at end of file
diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php
index f1dba552..35793cbb 100644
--- a/app/Template/analytic/layout.php
+++ b/app/Template/analytic/layout.php
@@ -1,36 +1,5 @@
<section id="main">
- <div class="page-header">
- <ul>
- <li>
- <span class="dropdown">
- <span>
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?= $this->render('project/dropdown', array('project' => $project)) ?>
- </ul>
- </span>
- </span>
- </li>
- <li>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <li>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $project['id'])): ?>
- <li>
- <i class="fa fa-cog fa-fw"></i>
- <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <?php endif ?>
- <li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
- </li>
- </ul>
- </div>
+ <?= $this->projectHeader->render($project, 'Listing', 'show') ?>
<section class="sidebar-container">
<?= $this->render($sidebar_template, array('project' => $project)) ?>
diff --git a/app/Template/app/layout.php b/app/Template/app/layout.php
index 200cb0d7..2a32ac02 100644
--- a/app/Template/app/layout.php
+++ b/app/Template/app/layout.php
@@ -1,5 +1,5 @@
<section id="main">
- <div class="page-header page-header-mobile">
+ <div class="page-header">
<ul>
<?php if ($this->user->hasAccess('ProjectCreation', 'create')): ?>
<li>
diff --git a/app/Template/avatar_file/show.php b/app/Template/avatar_file/show.php
new file mode 100644
index 00000000..266a2ccb
--- /dev/null
+++ b/app/Template/avatar_file/show.php
@@ -0,0 +1,20 @@
+<div class="page-header">
+ <h2><?= t('Avatar') ?></h2>
+</div>
+
+<?= $this->avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], '') ?>
+
+<form method="post" enctype="multipart/form-data" action="<?= $this->url->href('AvatarFile', 'upload', array('user_id' => $user['id'])) ?>">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->label(t('Upload my avatar image'), 'avatar') ?>
+ <?= $this->form->file('avatar') ?>
+
+ <div class="form-actions">
+ <?php if (! empty($user['avatar_path'])): ?>
+ <?= $this->url->link(t('Remove my image'), 'AvatarFile', 'remove', array('user_id' => $user['id']), true, 'btn btn-red') ?>
+ <?php endif ?>
+ <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
+ <?= t('or') ?>
+ <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?>
+ </div>
+</form>
diff --git a/app/Template/board/task_avatar.php b/app/Template/board/task_avatar.php
index 5630c190..39f6b54d 100644
--- a/app/Template/board/task_avatar.php
+++ b/app/Template/board/task_avatar.php
@@ -12,6 +12,7 @@
$task['assignee_username'],
$task['assignee_name'],
$task['assignee_email'],
+ $task['assignee_avatar_path'],
'avatar-inline'
) ?>
</span>
diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php
index a8203739..a9d381a3 100644
--- a/app/Template/board/task_footer.php
+++ b/app/Template/board/task_footer.php
@@ -76,6 +76,8 @@
<i class="fa fa-flag flag-milestone"></i>
</span>
<?php endif ?>
+
+ <?= $this->hook->render('template:board:task:icons', array('task' => $task)) ?>
<?= $this->task->formatPriority($project, $task) ?>
diff --git a/app/Template/board/view_private.php b/app/Template/board/view_private.php
index b5e38c66..13702273 100644
--- a/app/Template/board/view_private.php
+++ b/app/Template/board/view_private.php
@@ -1,19 +1,12 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- 'categories_list' => $categories_list,
- 'users_list' => $users_list,
- 'custom_filters_list' => $custom_filters_list,
- 'is_board' => true,
- )) ?>
+ <?= $this->projectHeader->render($project, 'Board', 'show', true) ?>
<?= $this->render('board/table_container', array(
- 'project' => $project,
- 'swimlanes' => $swimlanes,
- 'board_private_refresh_interval' => $board_private_refresh_interval,
- 'board_highlight_period' => $board_highlight_period,
+ 'project' => $project,
+ 'swimlanes' => $swimlanes,
+ 'board_private_refresh_interval' => $board_private_refresh_interval,
+ 'board_highlight_period' => $board_highlight_period,
)) ?>
</section>
diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php
index 7085b51e..f00e810b 100644
--- a/app/Template/calendar/show.php
+++ b/app/Template/calendar/show.php
@@ -1,9 +1,5 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- )) ?>
-
+ <?= $this->projectHeader->render($project, 'Calendar', 'show') ?>
<div id="calendar"
data-save-url="<?= $this->url->href('calendar', 'save', array('project_id' => $project['id'])) ?>"
data-check-url="<?= $this->url->href('calendar', 'project', array('project_id' => $project['id'])) ?>"
diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php
index ce456c5d..3f45e2e7 100644
--- a/app/Template/comment/show.php
+++ b/app/Template/comment/show.php
@@ -1,6 +1,6 @@
<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>">
- <?= $this->avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email']) ?>
+ <?= $this->avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email'], $comment['avatar_path']) ?>
<div class="comment-title">
<?php if (! empty($comment['username'])): ?>
diff --git a/app/Template/event/events.php b/app/Template/event/events.php
index ef651321..c58376c4 100644
--- a/app/Template/event/events.php
+++ b/app/Template/event/events.php
@@ -7,7 +7,8 @@
$event['creator_id'],
$event['author_username'],
$event['author_name'],
- $event['email']
+ $event['email'],
+ $event['avatar_path']
) ?>
<div class="activity-content">
diff --git a/app/Template/gantt/project.php b/app/Template/gantt/project.php
index fe193c2b..e6c8592f 100644
--- a/app/Template/gantt/project.php
+++ b/app/Template/gantt/project.php
@@ -1,10 +1,5 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- 'users_list' => $users_list,
- )) ?>
-
+ <?= $this->projectHeader->render($project, 'Gantt', 'project') ?>
<div class="menu-inline">
<ul>
<li <?= $sorting === 'board' ? 'class="active"' : '' ?>>
diff --git a/app/Template/listing/show.php b/app/Template/listing/show.php
index 10dcedcc..98b9528a 100644
--- a/app/Template/listing/show.php
+++ b/app/Template/listing/show.php
@@ -1,11 +1,5 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- 'custom_filters_list' => $custom_filters_list,
- 'users_list' => $users_list,
- 'categories_list' => $categories_list,
- )) ?>
+ <?= $this->projectHeader->render($project, 'Listing', 'show') ?>
<?php if ($paginator->isEmpty()): ?>
<p class="alert"><?= t('No tasks found.') ?></p>
@@ -24,7 +18,11 @@
<?php foreach ($paginator->getCollection() as $task): ?>
<tr>
<td class="task-table color-<?= $task['color_id'] ?>">
- <?= $this->render('task/dropdown', array('task' => $task)) ?>
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
+ <?= $this->render('task/dropdown', array('task' => $task)) ?>
+ <?php else: ?>
+ #<?= $task['id'] ?>
+ <?php endif ?>
</td>
<td>
<?= $this->text->e($task['swimlane_name'] ?: $task['default_swimlane']) ?>
diff --git a/app/Template/project/layout.php b/app/Template/project/layout.php
index eb391ae5..fcb3e5f3 100644
--- a/app/Template/project/layout.php
+++ b/app/Template/project/layout.php
@@ -1,30 +1,5 @@
<section id="main">
- <div class="page-header">
- <ul>
- <li>
- <span class="dropdown">
- <span>
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?= $this->render('project/dropdown', array('project' => $project)) ?>
- </ul>
- </span>
- </span>
- </li>
- <li>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <li>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?>
- </li>
- <li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
- </li>
- </ul>
- </div>
+ <?= $this->projectHeader->render($project, 'Listing', 'show') ?>
<section class="sidebar-container">
<?= $this->render($sidebar_template, array('project' => $project)) ?>
diff --git a/app/Template/project_header/dropdown.php b/app/Template/project_header/dropdown.php
index bbc033bf..759a5135 100644
--- a/app/Template/project_header/dropdown.php
+++ b/app/Template/project_header/dropdown.php
@@ -1,7 +1,7 @@
<div class="dropdown">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
+ <a href="#" class="dropdown-menu action-menu"><?= t('Menu') ?> <i class="fa fa-caret-down"></i></a>
<ul>
- <?php if ($is_board): ?>
+ <?php if ($board_view): ?>
<li>
<span class="filter-display-mode" <?= $this->board->isCollapsed($project['id']) ? '' : 'style="display: none;"' ?>>
<i class="fa fa-expand fa-fw"></i>
diff --git a/app/Template/project_header/header.php b/app/Template/project_header/header.php
index f6e5af9e..aaa8137b 100644
--- a/app/Template/project_header/header.php
+++ b/app/Template/project_header/header.php
@@ -1,7 +1,7 @@
<div class="project-header">
<?= $this->hook->render('template:project:header:before', array('project' => $project)) ?>
- <?= $this->render('project_header/dropdown', array('project' => $project, 'is_board' => isset($is_board))) ?>
+ <?= $this->render('project_header/dropdown', array('project' => $project, 'board_view' => $board_view)) ?>
<?= $this->render('project_header/views', array('project' => $project, 'filters' => $filters)) ?>
<?= $this->render('project_header/search', array(
'project' => $project,
diff --git a/app/Template/project_header/views.php b/app/Template/project_header/views.php
index f8fdbb02..353e4b62 100644
--- a/app/Template/project_header/views.php
+++ b/app/Template/project_header/views.php
@@ -1,7 +1,7 @@
<ul class="views">
<li <?= $this->app->getRouterController() === 'ProjectOverview' ? 'class="active"' : '' ?>>
<i class="fa fa-eye fa-fw"></i>
- <?= $this->url->link(t('Overview'), 'ProjectOverview', 'show', array('project_id' => $project['id']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?>
+ <?= $this->url->link(t('Overview'), 'ProjectOverview', 'show', array('project_id' => $project['id'], 'search' => $filters['search']), false, 'view-overview', t('Keyboard shortcut: "%s"', 'v o')) ?>
</li>
<li <?= $this->app->getRouterController() === 'Board' ? 'class="active"' : '' ?>>
<i class="fa fa-th fa-fw"></i>
diff --git a/app/Template/project_overview/description.php b/app/Template/project_overview/description.php
index 9895aede..72ccf06a 100644
--- a/app/Template/project_overview/description.php
+++ b/app/Template/project_overview/description.php
@@ -5,7 +5,7 @@
<div class="accordion-content">
<?php if ($this->user->hasProjectAccess('ProjectEdit', 'description', $project['id'])): ?>
<div class="buttons-header">
- <?= $this->url->button('fa-edit', t('Edit Description'), 'ProjectEdit', 'description', array('project_id' => $project['id']), 'popover') ?>
+ <?= $this->url->button('fa-edit', t('Edit description'), 'ProjectEdit', 'description', array('project_id' => $project['id']), 'popover') ?>
</div>
<?php endif ?>
<article class="markdown">
diff --git a/app/Template/project_overview/show.php b/app/Template/project_overview/show.php
index 9a9786e8..6fe815b3 100644
--- a/app/Template/project_overview/show.php
+++ b/app/Template/project_overview/show.php
@@ -1,9 +1,5 @@
<section id="main">
- <?= $this->render('project_header/header', array(
- 'project' => $project,
- 'filters' => $filters,
- )) ?>
-
+ <?= $this->projectHeader->render($project, 'ProjectOverview', 'show') ?>
<?= $this->render('project_overview/columns', array('project' => $project)) ?>
<?= $this->render('project_overview/description', array('project' => $project)) ?>
<?= $this->render('project_overview/attachments', array('project' => $project, 'images' => $images, 'files' => $files)) ?>
diff --git a/app/Template/search/activity.php b/app/Template/search/activity.php
new file mode 100644
index 00000000..60362215
--- /dev/null
+++ b/app/Template/search/activity.php
@@ -0,0 +1,39 @@
+<section id="main">
+ <div class="page-header">
+ <ul>
+ <li>
+ <i class="fa fa-search fa-fw"></i>
+ <?= $this->url->link(t('Search tasks'), 'search', 'index') ?>
+ </li>
+ </ul>
+ </div>
+
+ <div class="filter-box">
+ <form method="get" action="<?= $this->url->dir() ?>" class="search">
+ <?= $this->form->hidden('controller', $values) ?>
+ <?= $this->form->hidden('action', $values) ?>
+ <?= $this->form->text('search', $values, array(), array(empty($values['search']) ? 'autofocus' : '', 'placeholder="'.t('Search').'"'), 'form-input-large') ?>
+ <?= $this->render('activity/filter_dropdown') ?>
+ </form>
+ </div>
+
+ <?php if (empty($values['search'])): ?>
+ <div class="listing">
+ <h3><?= t('Advanced search') ?></h3>
+ <p><?= t('Example of query: ') ?><strong>project:"My project" creator:me</strong></p>
+ <ul>
+ <li><?= t('Search by project: ') ?><strong>project:"My project"</strong></li>
+ <li><?= t('Search by creator: ') ?><strong>creator:admin</strong></li>
+ <li><?= t('Search by creation date: ') ?><strong>created:today</strong></li>
+ <li><?= t('Search by task status: ') ?><strong>status:open</strong></li>
+ <li><?= t('Search by task title: ') ?><strong>title:"My task"</strong></li>
+ </ul>
+ <p><i class="fa fa-external-link fa-fw"></i><?= $this->url->doc(t('View advanced search syntax'), 'search') ?></p>
+ </div>
+ <?php elseif (! empty($values['search']) && $nb_events === 0): ?>
+ <p class="alert"><?= t('Nothing found.') ?></p>
+ <?php else: ?>
+ <?= $this->render('event/events', array('events' => $events)) ?>
+ <?php endif ?>
+
+</section> \ No newline at end of file
diff --git a/app/Template/search/index.php b/app/Template/search/index.php
index 9231a6f3..d5d07ed6 100644
--- a/app/Template/search/index.php
+++ b/app/Template/search/index.php
@@ -2,8 +2,8 @@
<div class="page-header">
<ul>
<li>
- <i class="fa fa-folder fa-fw"></i>
- <?= $this->url->link(t('All projects'), 'project', 'index') ?>
+ <i class="fa fa-search fa-fw"></i>
+ <?= $this->url->link(t('Activity stream search'), 'search', 'activity') ?>
</li>
</ul>
</div>
diff --git a/app/Template/task/details.php b/app/Template/task/details.php
index a7c4ad01..6093c157 100644
--- a/app/Template/task/details.php
+++ b/app/Template/task/details.php
@@ -1,5 +1,6 @@
<section id="task-summary">
<h2><?= $this->text->e($task['title']) ?></h2>
+
<div class="task-summary-container color-<?= $task['color_id'] ?>">
<div class="task-summary-column">
<ul class="no-bullet">
@@ -134,4 +135,10 @@
</ul>
</div>
</div>
+
+ <?php if ($editable && empty($task['date_started'])): ?>
+ <div class="task-summary-buttons">
+ <?= $this->url->button('fa-play', t('Set start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </div>
+ <?php endif ?>
</section>
diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php
index ca27fd4f..52db5d1b 100644
--- a/app/Template/task/layout.php
+++ b/app/Template/task/layout.php
@@ -1,25 +1,5 @@
<section id="main">
- <div class="page-header">
- <ul>
- <li>
- <?= $this->render('task/menu', array('task' => $task)) ?>
- </li>
- <li>
- <i class="fa fa-th fa-fw"></i>
- <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, $task['swimlane_id'] != 0 ? 'swimlane-'.$task['swimlane_id'] : '') ?>
- </li>
- <li>
- <i class="fa fa-calendar fa-fw"></i>
- <?= $this->url->link(t('Back to the calendar'), 'calendar', 'show', array('project_id' => $task['project_id'])) ?>
- </li>
- <?php if ($this->user->hasProjectAccess('ProjectEdit', 'edit', $task['project_id'])): ?>
- <li>
- <i class="fa fa-cog fa-fw"></i>
- <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $task['project_id'])) ?>
- </li>
- <?php endif ?>
- </ul>
- </div>
+ <?= $this->projectHeader->render($project, 'Listing', 'show') ?>
<section
class="sidebar-container" id="task-view"
data-edit-url="<?= $this->url->href('taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"
diff --git a/app/Template/task/menu.php b/app/Template/task/menu.php
deleted file mode 100644
index fe30d06e..00000000
--- a/app/Template/task/menu.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
-<div class="dropdown">
- <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a>
- <ul>
- <?php if (empty($task['date_started'])): ?>
- <li>
- <i class="fa fa-play fa-fw"></i>
- <?= $this->url->link(t('Set automatically the start date'), 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
- </li>
- <?php endif ?>
- <li>
- <i class="fa fa-pencil-square-o fa-fw"></i>
- <?= $this->url->link(t('Edit the task'), 'taskmodification', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-align-left fa-fw"></i>
- <?= $this->url->link(t('Edit the description'), 'taskmodification', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-refresh fa-rotate-90 fa-fw"></i>
- <?= $this->url->link(t('Edit recurrence'), 'TaskRecurrence', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-plus fa-fw"></i>
- <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-code-fork fa-fw"></i>
- <?= $this->url->link(t('Add internal link'), 'TaskInternalLink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-external-link fa-fw"></i>
- <?= $this->url->link(t('Add external link'), 'TaskExternalLink', 'find', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-comment-o fa-fw"></i>
- <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-file fa-fw"></i>
- <?= $this->url->link(t('Attach a document'), 'TaskFile', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-camera fa-fw"></i>
- <?= $this->url->link(t('Add a screenshot'), 'TaskFile', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-files-o fa-fw"></i>
- <?= $this->url->link(t('Duplicate'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-clipboard fa-fw"></i>
- <?= $this->url->link(t('Duplicate to another project'), 'taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <i class="fa fa-clone fa-fw"></i>
- <?= $this->url->link(t('Move to another project'), 'taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <li>
- <?php if ($task['is_active'] == 1): ?>
- <i class="fa fa-times fa-fw"></i>
- <?= $this->url->link(t('Close this task'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- <?php else: ?>
- <i class="fa fa-check-square-o fa-fw"></i>
- <?= $this->url->link(t('Open this task'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- <?php endif ?>
- </li>
- <?php if ($this->task->canRemove($task)): ?>
- <li>
- <i class="fa fa-trash-o fa-fw"></i>
- <?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'popover') ?>
- </li>
- <?php endif ?>
-
- <?= $this->hook->render('template:task:menu') ?>
- </ul>
-</div>
-<?php endif ?>
diff --git a/app/Template/task/show.php b/app/Template/task/show.php
index d68f6c48..86422941 100644
--- a/app/Template/task/show.php
+++ b/app/Template/task/show.php
@@ -14,7 +14,6 @@
'task' => $task,
'subtasks' => $subtasks,
'project' => $project,
- 'users_list' => isset($users_list) ? $users_list : array(),
'editable' => true,
)) ?>
diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php
index ee3b1594..773b28dc 100644
--- a/app/Template/task/sidebar.php
+++ b/app/Template/task/sidebar.php
@@ -24,6 +24,8 @@
</li>
<?php endif ?>
</ul>
+
+ <?php if ($this->user->hasProjectAccess('taskmodification', 'edit', $task['project_id'])): ?>
<h2><?= t('Actions') ?></h2>
<ul>
<li>
@@ -90,6 +92,7 @@
</li>
<?php endif ?>
</ul>
+ <?php endif ?>
<?= $this->hook->render('template:task:sidebar', array('task' => $task)) ?>
</div>
diff --git a/app/Template/task_creation/form.php b/app/Template/task_creation/form.php
index cecba9ef..9bfd839f 100644
--- a/app/Template/task_creation/form.php
+++ b/app/Template/task_creation/form.php
@@ -28,6 +28,8 @@
<?php if (! isset($duplicate)): ?>
<?= $this->form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?>
<?php endif ?>
+
+ <?= $this->hook->render('template:task:form:left-column', array('values'=>$values, 'errors'=>$errors)) ?>
</div>
<div class="form-column">
@@ -48,4 +50,4 @@
<button type="submit" class="btn btn-blue" tabindex="15"><?= t('Save') ?></button>
<?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?>
</div>
-</form> \ No newline at end of file
+</form>
diff --git a/app/Template/user/index.php b/app/Template/user/index.php
index 364fd965..0b5da17c 100644
--- a/app/Template/user/index.php
+++ b/app/Template/user/index.php
@@ -14,6 +14,7 @@
<?php else: ?>
<table class="table-stripped">
<tr>
+ <th class="column-5"><?= $paginator->order(t('Id'), 'id') ?></th>
<th class="column-18"><?= $paginator->order(t('Username'), 'username') ?></th>
<th class="column-18"><?= $paginator->order(t('Name'), 'name') ?></th>
<th class="column-15"><?= $paginator->order(t('Email'), 'email') ?></th>
@@ -26,7 +27,9 @@
<?php foreach ($paginator->getCollection() as $user): ?>
<tr>
<td>
- <?= '#'.$user['id'] ?>&nbsp;
+ <?= '#'.$user['id'] ?>
+ </td>
+ <td>
<?= $this->url->link($this->text->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?>
</td>
<td>
diff --git a/app/Template/user/notifications.php b/app/Template/user/notifications.php
index 2a5c8152..6e1a0004 100644
--- a/app/Template/user/notifications.php
+++ b/app/Template/user/notifications.php
@@ -3,11 +3,8 @@
</div>
<form method="post" action="<?= $this->url->href('user', 'notifications', array('user_id' => $user['id'])) ?>" autocomplete="off">
-
<?= $this->form->csrf() ?>
- <?= $this->form->checkbox('notifications_enabled', t('Enable notifications'), '1', $notifications['notifications_enabled'] == 1) ?><br>
- <hr>
<h4><?= t('Notification methods:') ?></h4>
<?= $this->form->checkboxes('notification_types', $types, $notifications) ?>
diff --git a/app/Template/user/profile.php b/app/Template/user/profile.php
index 80a633e3..9c9d3282 100644
--- a/app/Template/user/profile.php
+++ b/app/Template/user/profile.php
@@ -1,5 +1,6 @@
<section id="main">
<br>
+ <?= $this->avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path']) ?>
<ul class="listing">
<li><?= t('Username:') ?> <strong><?= $this->text->e($user['username']) ?></strong></li>
<li><?= t('Name:') ?> <strong><?= $this->text->e($user['name']) ?: t('None') ?></strong></li>
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index 20fd2ad2..5ea2e355 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -37,6 +37,9 @@
<li <?= $this->app->checkMenuSelection('user', 'edit') ?>>
<?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
</li>
+ <li <?= $this->app->checkMenuSelection('AvatarFile') ?>>
+ <?= $this->url->link(t('Avatar'), 'AvatarFile', 'show', array('user_id' => $user['id'])) ?>
+ </li>
<?php endif ?>
<?php if ($user['is_ldap_user'] == 0): ?>
diff --git a/app/User/Avatar/AvatarFileProvider.php b/app/User/Avatar/AvatarFileProvider.php
new file mode 100644
index 00000000..eea565f0
--- /dev/null
+++ b/app/User/Avatar/AvatarFileProvider.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Kanboard\User\Avatar;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\User\Avatar\AvatarProviderInterface;
+
+/**
+ * Avatar Local Image File Provider
+ *
+ * @package avatar
+ * @author Frederic Guillot
+ */
+class AvatarFileProvider extends Base implements AvatarProviderInterface
+{
+ /**
+ * Render avatar html
+ *
+ * @access public
+ * @param array $user
+ * @param int $size
+ * @return string
+ */
+ public function render(array $user, $size)
+ {
+ $url = $this->helper->url->href('AvatarFile', 'image', array('user_id' => $user['id'], 'size' => $size));
+ $title = $this->helper->text->e($user['name'] ?: $user['username']);
+ return '<img src="' . $url . '" alt="' . $title . '" title="' . $title . '">';
+ }
+
+ /**
+ * Determine if the provider is active
+ *
+ * @access public
+ * @param array $user
+ * @return boolean
+ */
+ public function isActive(array $user)
+ {
+ return !empty($user['avatar_path']);
+ }
+}
diff --git a/app/User/Avatar/GravatarProvider.php b/app/User/Avatar/GravatarProvider.php
index 7a719734..87ca51b1 100644
--- a/app/User/Avatar/GravatarProvider.php
+++ b/app/User/Avatar/GravatarProvider.php
@@ -17,8 +17,9 @@ class GravatarProvider extends Base implements AvatarProviderInterface
* Render avatar html
*
* @access public
- * @param array $user
- * @param int $size
+ * @param array $user
+ * @param int $size
+ * @return string
*/
public function render(array $user, $size)
{
diff --git a/app/User/Avatar/LetterAvatarProvider.php b/app/User/Avatar/LetterAvatarProvider.php
index 81c4586d..f9659e61 100644
--- a/app/User/Avatar/LetterAvatarProvider.php
+++ b/app/User/Avatar/LetterAvatarProvider.php
@@ -24,13 +24,14 @@ class LetterAvatarProvider extends Base implements AvatarProviderInterface
* Render avatar html
*
* @access public
- * @param array $user
- * @param int $size
+ * @param array $user
+ * @param int $size
+ * @return string
*/
public function render(array $user, $size)
{
$initials = $this->helper->user->getInitials($user['name'] ?: $user['username']);
- $rgb = $this->getBackgroundColor($initials);
+ $rgb = $this->getBackgroundColor($user['name'] ?: $user['username']);
return sprintf(
'<div class="avatar-letter" style="background-color: rgb(%d, %d, %d)" title="%s">%s</div>',
diff --git a/app/common.php b/app/common.php
index 7dbd7587..da624844 100644
--- a/app/common.php
+++ b/app/common.php
@@ -39,4 +39,5 @@ $container->register(new Kanboard\ServiceProvider\RouteProvider);
$container->register(new Kanboard\ServiceProvider\ActionProvider);
$container->register(new Kanboard\ServiceProvider\ExternalLinkProvider);
$container->register(new Kanboard\ServiceProvider\AvatarProvider);
+$container->register(new Kanboard\ServiceProvider\FilterProvider);
$container->register(new Kanboard\ServiceProvider\PluginProvider);