From 354e37971d43d3b62d8f4e2a23eff09e88525627 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Fri, 25 Mar 2016 17:41:41 -0400 Subject: Unification of project header --- ChangeLog | 1 + 1 file changed, 1 insertion(+) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 54a28db7..327d2389 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,7 @@ New features: Improvements: +* Unification of the project header * Refactoring of Javascript code * Improve comments design * Improve task summary sections -- cgit v1.2.3 From 407a51e6c45f411533b13176a614ed28e7cd460d Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Fri, 25 Mar 2016 18:19:31 -0400 Subject: Allow to use the original template in overridden templates (PR #1941) --- ChangeLog | 1 + app/Core/Template.php | 25 +++++++++++++------------ doc/plugin-overrides.markdown | 6 ++++++ tests/units/Core/TemplateTest.php | 30 ++++++++++++++++++------------ 4 files changed, 38 insertions(+), 24 deletions(-) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 327d2389..5fdbf6e3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,7 @@ New features: Improvements: +* Allow to use the original template in overridden templates * Unification of the project header * Refactoring of Javascript code * Improve comments design diff --git a/app/Core/Template.php b/app/Core/Template.php index f85c7f28..cf5512d9 100644 --- a/app/Core/Template.php +++ b/app/Core/Template.php @@ -84,25 +84,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_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 (strpos($template, ':') !== false) { + list($plugin, $template) = explode(':', $template); } - return $path; + if ($plugin !== 'kanboard' && $plugin !== '') { + return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', '..', 'plugins', ucfirst($plugin), 'Template', $template.'.php')); + } + + return implode(DIRECTORY_SEPARATOR, array(__DIR__, '..', 'Template', $template.'.php')); } } diff --git a/doc/plugin-overrides.markdown b/doc/plugin-overrides.markdown index 722b4126..96a09e47 100644 --- a/doc/plugin-overrides.markdown +++ b/doc/plugin-overrides.markdown @@ -34,3 +34,9 @@ $this->template->setTemplateOverride('header', 'theme:layout/header'); ``` The first argument is the original template name and the second argument the template to use as replacement. + +You can still use the original template using the "kanboard:" prefix: + +```php +render('kanboard:header') ?> +``` diff --git a/tests/units/Core/TemplateTest.php b/tests/units/Core/TemplateTest.php index bd476c51..9584c831 100644 --- a/tests/units/Core/TemplateTest.php +++ b/tests/units/Core/TemplateTest.php @@ -8,35 +8,41 @@ class TemplateTest extends Base { public function testGetTemplateFile() { - $t = new Template($this->container['helper']); + $template = new Template($this->container['helper']); + + $this->assertStringEndsWith( + implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'a', 'b.php')), + $template->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b') + ); + $this->assertStringEndsWith( - 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'a'.DIRECTORY_SEPARATOR.'b.php', - $t->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b') + implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'a', 'b.php')), + $template->getTemplateFile('kanboard:a'.DIRECTORY_SEPARATOR.'b') ); } public function testGetPluginTemplateFile() { - $t = new Template($this->container['helper']); + $template = new Template($this->container['helper']); $this->assertStringEndsWith( - 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins'.DIRECTORY_SEPARATOR.'Myplugin'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'a'.DIRECTORY_SEPARATOR.'b.php', - $t->getTemplateFile('myplugin:a'.DIRECTORY_SEPARATOR.'b') + implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', '..', 'plugins', 'Myplugin', 'Template', 'a', 'b.php')), + $template->getTemplateFile('myplugin:a'.DIRECTORY_SEPARATOR.'b') ); } public function testGetOverridedTemplateFile() { - $t = new Template($this->container['helper']); - $t->setTemplateOverride('a'.DIRECTORY_SEPARATOR.'b', 'myplugin:c'); + $template = new Template($this->container['helper']); + $template->setTemplateOverride('a'.DIRECTORY_SEPARATOR.'b', 'myplugin:c'); $this->assertStringEndsWith( - 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'plugins'.DIRECTORY_SEPARATOR.'Myplugin'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'c.php', - $t->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b') + implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', '..', 'plugins', 'Myplugin', 'Template', 'c.php')), + $template->getTemplateFile('a'.DIRECTORY_SEPARATOR.'b') ); $this->assertStringEndsWith( - 'app'.DIRECTORY_SEPARATOR.'Core'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Template'.DIRECTORY_SEPARATOR.'d.php', - $t->getTemplateFile('d') + implode(DIRECTORY_SEPARATOR, array('app', 'Core', '..', 'Template', 'd.php')), + $template->getTemplateFile('d') ); } } -- cgit v1.2.3 From c7cceade96d2698d2684add1970c03c8b4f32dfc Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 12:23:18 -0400 Subject: Handle state in OAuth2 client --- ChangeLog | 1 + app/Controller/Oauth.php | 106 ++++++++++++++++++++------------- app/Core/Http/OAuth2.php | 45 +++++++++++--- app/Core/Session/SessionStorage.php | 1 + app/Locale/bs_BA/translations.php | 1 + app/Locale/cs_CZ/translations.php | 1 + app/Locale/da_DK/translations.php | 1 + app/Locale/de_DE/translations.php | 1 + app/Locale/el_GR/translations.php | 1 + app/Locale/es_ES/translations.php | 1 + app/Locale/fi_FI/translations.php | 1 + app/Locale/fr_FR/translations.php | 1 + app/Locale/hu_HU/translations.php | 1 + app/Locale/id_ID/translations.php | 1 + app/Locale/it_IT/translations.php | 1 + app/Locale/ja_JP/translations.php | 1 + app/Locale/my_MY/translations.php | 1 + app/Locale/nb_NO/translations.php | 1 + app/Locale/nl_NL/translations.php | 1 + app/Locale/pl_PL/translations.php | 1 + app/Locale/pt_BR/translations.php | 1 + app/Locale/pt_PT/translations.php | 1 + app/Locale/ru_RU/translations.php | 1 + app/Locale/sr_Latn_RS/translations.php | 1 + app/Locale/sv_SE/translations.php | 1 + app/Locale/th_TH/translations.php | 1 + app/Locale/tr_TR/translations.php | 1 + app/Locale/zh_CN/translations.php | 1 + tests/units/Core/Http/OAuth2Test.php | 7 ++- 29 files changed, 133 insertions(+), 51 deletions(-) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 5fdbf6e3..5fded0d4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,7 @@ New features: Improvements: +* Handle state in OAuth2 client * Allow to use the original template in overridden templates * Unification of the project header * Refactoring of Javascript code 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 * @@ -10,25 +12,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 * @@ -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,57 +34,97 @@ 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.')); } $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); } + /** + * 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/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 @@ -45,6 +45,33 @@ class OAuth2 extends Base return $this; } + /** + * 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 * @@ -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/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/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index 8d653d4f..7ca864f4 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index 3606eddf..b2921de9 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index cf3f0191..c4743922 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 1090d6c9..af88b374 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index 04efa7e7..9a31e485 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 477f3655..c3623369 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index a32082e3..8e5dd81f 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index 00e64876..cedd6039 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -1152,4 +1152,5 @@ return array( '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', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index f2e1cafb..f642a6c1 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index 8d279633..3f105054 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 87327462..93ceb03f 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index aa8cc654..b48eabd8 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index be41c19c..36b3db0b 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 0e214cf4..465efb53 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index dc68eb34..3c3fa1ee 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 0d020dcb..d06e347f 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index ebed08cd..050d1a9f 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 4d2d20b4..1c327887 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 1d93b3f3..3cb3c6bb 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 634f6f8c..c7070a8d 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 4dcc63ad..e4728d2d 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index a81bef73..1e2fb98a 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index 9a5380d2..6e8fae2f 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index d7e45a89..decd49d8 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -1152,4 +1152,5 @@ return array( // 'Avatar' => '', // 'Upload my avatar image' => '', // 'Remove my image' => '', + // 'The OAuth2 state parameter is invalid' => '', ); diff --git a/tests/units/Core/Http/OAuth2Test.php b/tests/units/Core/Http/OAuth2Test.php index c68ae116..5a9c0ac1 100644 --- a/tests/units/Core/Http/OAuth2Test.php +++ b/tests/units/Core/Http/OAuth2Test.php @@ -10,7 +10,8 @@ class OAuth2Test extends Base { $oauth = new OAuth2($this->container); $oauth->createService('A', 'B', 'C', 'D', 'E', array('f', 'g')); - $this->assertEquals('D?response_type=code&client_id=A&redirect_uri=C&scope=f+g', $oauth->getAuthorizationUrl()); + $state = $oauth->getState(); + $this->assertEquals('D?response_type=code&client_id=A&redirect_uri=C&scope=f+g&state='.$state, $oauth->getAuthorizationUrl()); } public function testAuthHeader() @@ -27,12 +28,15 @@ class OAuth2Test extends Base public function testAccessToken() { + $oauth = new OAuth2($this->container); + $params = array( 'code' => 'something', 'client_id' => 'A', 'client_secret' => 'B', 'redirect_uri' => 'C', 'grant_type' => 'authorization_code', + 'state' => $oauth->getState(), ); $response = json_encode(array( @@ -46,7 +50,6 @@ class OAuth2Test extends Base ->with('E', $params, array('Accept: application/json')) ->will($this->returnValue($response)); - $oauth = new OAuth2($this->container); $oauth->createService('A', 'B', 'C', 'D', 'E', array('f', 'g')); $oauth->getAccessToken('something'); } -- cgit v1.2.3 From f11fccd0d78ab037e77cd973a9168eedcb609fc2 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 15:32:29 -0400 Subject: Fix bad unique constraints in Mysql table user_has_notifications --- ChangeLog | 2 ++ app/Model/UserNotification.php | 19 ++++++++----------- app/Model/UserNotificationFilter.php | 9 +++++++-- app/Schema/Mysql.php | 11 ++++++++++- app/Template/user/notifications.php | 3 --- tests/units/Model/UserNotificationFilterTest.php | 5 +++-- 6 files changed, 30 insertions(+), 19 deletions(-) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 5fded0d4..0da552ed 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,7 @@ New features: Improvements: +* Improve notification configuration form * Handle state in OAuth2 client * Allow to use the original template in overridden templates * Unification of the project header @@ -30,6 +31,7 @@ Improvements: Bug fixes: +* Fix bad unique constraints in Mysql table user_has_notifications * Force integer type for aggregated metrics (Burndown chart concat values instead of summing) * Fixes cycle time calculation when the start date is defined in the future * Access allowed to any tasks from the shared public board by changing the URL parameters diff --git a/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 ccb5a9cf..a041b3dc 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,16 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 109; +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`)"); +} function version_109(PDO $pdo) { 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 @@
- form->csrf() ?> - form->checkbox('notifications_enabled', t('Enable notifications'), '1', $notifications['notifications_enabled'] == 1) ?>
-

form->checkboxes('notification_types', $types, $notifications) ?> diff --git a/tests/units/Model/UserNotificationFilterTest.php b/tests/units/Model/UserNotificationFilterTest.php index 0b5f1d98..924f0883 100644 --- a/tests/units/Model/UserNotificationFilterTest.php +++ b/tests/units/Model/UserNotificationFilterTest.php @@ -26,10 +26,11 @@ class UserNotificationFilterTest extends Base $this->assertEquals(1, $p->create(array('name' => 'UnitTest1'))); $this->assertEquals(2, $p->create(array('name' => 'UnitTest2'))); + $this->assertEquals(3, $p->create(array('name' => 'UnitTest3'))); $this->assertEmpty($nf->getSelectedProjects(1)); - $nf->saveSelectedProjects(1, array(1, 2)); - $this->assertEquals(array(1, 2), $nf->getSelectedProjects(1)); + $this->assertTrue($nf->saveSelectedProjects(1, array(1, 2, 3))); + $this->assertEquals(array(1, 2, 3), $nf->getSelectedProjects(1)); } public function testSaveUserFilter() -- cgit v1.2.3 From 004fde30f7cc0dd4864c50f8d804da9679f524fa Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 16:21:15 -0400 Subject: Update SQL dumps and improve schema migration process --- ChangeLog | 1 + app/Schema/Mysql.php | 2 + app/Schema/Sql/mysql.sql | 17 ++++++-- app/Schema/Sql/postgres.sql | 67 ++++++++++++++++++-------------- app/ServiceProvider/DatabaseProvider.php | 4 +- composer.json | 2 +- composer.lock | 54 ++++++++++++------------- 7 files changed, 84 insertions(+), 63 deletions(-) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 0da552ed..000ab61b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,7 @@ New features: Improvements: +* Improve schema migration process * Improve notification configuration form * Handle state in OAuth2 client * Allow to use the original template in overridden templates diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index a041b3dc..934b063f 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -15,6 +15,8 @@ function version_110(PDO $pdo) $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) 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/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/composer.json b/composer.json index 969b0606..5dd9a4cf 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "eluceo/ical": "0.8.0", "erusev/parsedown" : "1.6.0", "fguillot/json-rpc" : "1.0.3", - "fguillot/picodb" : "1.0.7", + "fguillot/picodb" : "1.0.8", "fguillot/simpleLogger" : "1.0.0", "fguillot/simple-validator" : "1.0.0", "paragonie/random_compat": "@stable", diff --git a/composer.lock b/composer.lock index caeb4906..438118a2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "d60e4b6b7ceb60202c48112ffbc33fba", - "content-hash": "fbb704fa621ed6dd3c60241913ea1686", + "hash": "ecdd93c089273876816339ff22d67cc7", + "content-hash": "a5edc6f9c9ae2cd356e3f8ac96ef5532", "packages": [ { "name": "christian-riesen/base32", @@ -239,16 +239,16 @@ }, { "name": "fguillot/picodb", - "version": "v1.0.7", + "version": "v1.0.8", "source": { "type": "git", "url": "https://github.com/fguillot/picoDb.git", - "reference": "7f36dc3a7814ca0fc63439cd948e8acfeda672de" + "reference": "672a819ba2757a9e22a3572a230e735e84bcf625" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/picoDb/zipball/7f36dc3a7814ca0fc63439cd948e8acfeda672de", - "reference": "7f36dc3a7814ca0fc63439cd948e8acfeda672de", + "url": "https://api.github.com/repos/fguillot/picoDb/zipball/672a819ba2757a9e22a3572a230e735e84bcf625", + "reference": "672a819ba2757a9e22a3572a230e735e84bcf625", "shasum": "" }, "require": { @@ -272,7 +272,7 @@ ], "description": "Minimalist database query builder", "homepage": "https://github.com/fguillot/picoDb", - "time": "2016-03-12 14:31:33" + "time": "2016-03-27 20:15:25" }, { "name": "fguillot/simple-validator", @@ -397,16 +397,16 @@ }, { "name": "paragonie/random_compat", - "version": "v1.2.2", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "b3313b618f4edd76523572531d5d7e22fe747430" + "reference": "76e90f747b769b347fe584e8015a014549107d35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/b3313b618f4edd76523572531d5d7e22fe747430", - "reference": "b3313b618f4edd76523572531d5d7e22fe747430", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/76e90f747b769b347fe584e8015a014549107d35", + "reference": "76e90f747b769b347fe584e8015a014549107d35", "shasum": "" }, "require": { @@ -441,7 +441,7 @@ "pseudorandom", "random" ], - "time": "2016-03-11 19:54:08" + "time": "2016-03-18 20:36:13" }, { "name": "pimple/pimple", @@ -627,16 +627,16 @@ }, { "name": "symfony/console", - "version": "v2.8.3", + "version": "v2.8.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "56cc5caf051189720b8de974e4746090aaa10d44" + "reference": "9a5aef5fc0d4eff86853d44202b02be8d5a20154" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/56cc5caf051189720b8de974e4746090aaa10d44", - "reference": "56cc5caf051189720b8de974e4746090aaa10d44", + "url": "https://api.github.com/repos/symfony/console/zipball/9a5aef5fc0d4eff86853d44202b02be8d5a20154", + "reference": "9a5aef5fc0d4eff86853d44202b02be8d5a20154", "shasum": "" }, "require": { @@ -683,20 +683,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-02-28 16:20:50" + "time": "2016-03-17 09:19:04" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.3", + "version": "v2.8.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "78c468665c9568c3faaa9c416a7134308f2d85c3" + "reference": "47d2d8cade9b1c3987573d2943bb9352536cdb87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/78c468665c9568c3faaa9c416a7134308f2d85c3", - "reference": "78c468665c9568c3faaa9c416a7134308f2d85c3", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/47d2d8cade9b1c3987573d2943bb9352536cdb87", + "reference": "47d2d8cade9b1c3987573d2943bb9352536cdb87", "shasum": "" }, "require": { @@ -743,7 +743,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-01-27 05:14:19" + "time": "2016-03-07 14:04:32" }, { "name": "symfony/polyfill-mbstring", @@ -808,16 +808,16 @@ "packages-dev": [ { "name": "symfony/stopwatch", - "version": "v2.8.3", + "version": "v2.8.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "e3bc8e2a984f4382690a438c8bb650f3ffd71e73" + "reference": "9e24824b2a9a16e17ab997f61d70bc03948e434e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e3bc8e2a984f4382690a438c8bb650f3ffd71e73", - "reference": "e3bc8e2a984f4382690a438c8bb650f3ffd71e73", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/9e24824b2a9a16e17ab997f61d70bc03948e434e", + "reference": "9e24824b2a9a16e17ab997f61d70bc03948e434e", "shasum": "" }, "require": { @@ -853,7 +853,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2016-01-03 15:33:41" + "time": "2016-03-04 07:54:35" } ], "aliases": [], -- cgit v1.2.3 From 0c4a5afa839187f66af93ef37dfb08677aeb5b59 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 16:29:33 -0400 Subject: Integrate Korean translation --- CONTRIBUTORS.md | 1 + ChangeLog | 1 + app/Locale/ko_KR/translations.php | 1466 +++++++++++++++++++------------------ app/Model/Config.php | 3 +- 4 files changed, 760 insertions(+), 711 deletions(-) (limited to 'ChangeLog') diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f4576949..61d190fa 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -120,6 +120,7 @@ Contributors: - [Vladimir Babin](https://github.com/Chiliec) - [Yannick Ihmels](https://github.com/ihmels) - [Ybarc](https://github.com/ybarc) +- [Yu Yongwoo](https://github.com/uyu423) - [Yuichi Murata](https://github.com/yuichi1004) There is also many people who have reported bugs or proposed awesome ideas. \ No newline at end of file diff --git a/ChangeLog b/ChangeLog index 000ab61b..c732f89a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,7 @@ New features: * Added Markdown editor * Added letter avatar provider * Added pluggable Avatar providers +* Added Korean translation Improvements: diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php index ad1a62d6..8379761f 100644 --- a/app/Locale/ko_KR/translations.php +++ b/app/Locale/ko_KR/translations.php @@ -1,686 +1,631 @@ '', - //'number.thousands_separator'=>'', - 'None'=>'없음', - 'edit'=>'수정', - 'Edit'=>'수정', - 'remove'=>'삭제', - 'Remove'=>'삭제', - 'Update'=>'수정', - 'Yes'=>'예', - 'No'=>'아니오', - 'cancel'=>'취소', - 'or'=>'또는', - 'Yellow'=>'노랑', - 'Blue'=>'파랑', - 'Green'=>'초록', - 'Purple'=>'보라', - 'Red'=>'빨강', - 'Orange'=>'주황', - 'Grey'=>'회색', - 'Brown'=>'브라운', - // 'Deep Orange'=>'오아', - // 'Dark Grey'=>'', - // 'Pink'=>'', - // 'Teal'=>'', - // 'Cyan'=>'', - // 'Lime'=>'', - // 'Light Green'=>'', - // 'Amber'=>'', - 'Save'=>'저장', - 'Login'=>'로그인', - 'Official website:'=>'공식 웹사이트:', - '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'=>'유효', - 'Add this column'=>'칼럼을 추가하는 ', - '%d tasks on the board'=>'%d개의 할일', - '%d tasks in total'=>'총 %d개의 할일', - 'Unable to update this board.'=>'보드를 갱신할 수 없었습니다', - 'Edit board'=>'보드를 변경하는 ', - 'Disable'=>'비활성화', - 'Enable'=>'유효하게 한다', - 'New project'=>'새 프로젝트', - 'Do you really want to remove this project:"%s"?'=>'프로젝트"%s"를 정말로 삭제하시겠습니까?', - 'Remove project'=>'프로젝트의 삭제', - 'Edit the board for"%s"'=>'보드"%s"를 변경하는 ', - 'All projects'=>'모든 프로젝트', - 'Change columns'=>'칼럼의 변경', - 'Add a new column'=>'칼럼의 추가', - 'Title'=>'제목', - 'Nobody assigned'=>'담당자 없음', - 'Assigned to %s'=>'담당자 %s', - 'Remove a column'=>'칼럼 삭제', - 'Remove a column from a board'=>'보드에서 칼럼 삭제', - 'Unable to remove this column.'=>'(※)컬럼을 삭제할 수 없었습니다.', - 'Do you really want to remove this column:"%s"('=>'칼럼"%s"을 삭제할까요?', - 'This action will REMOVE ALL TASKS associated to this column!'=>'이 조작은 이 컬럼에 할당된 『 모든 할일을 삭제 』합니다!', - 'Settings'=>'설정', - 'Application settings'=>'애플리케이션의 설정', - 'Language'=>'언어', - 'Webhook token:'=>'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"?'=>'할일"%s"를 오픈합니까?', - 'Back to the board'=>'보드로 돌아가기', - 'Created on %B %e, %Y at %k:%M %p'=>'%Y-%m-%d %p %H:%M 에 작성', - 'There is nobody assigned'=>'담당자가 없습니다', - 'Column on the board:'=>'칼럼:', - 'Status is open'=>'상태 열림', - 'Status is closed'=>'상태 닫힘', - 'Close this task'=>'할일 마치기', - 'Open this task'=>'할일을 열다', - 'There is no description.'=>'설명이 없다', - 'Add a new task'=>'할일을 추가하는 ', - 'The username is required'=>'사용자 이름이 필요합니다', - 'The maximum length is%d characters'=>'최대%d문자에요!', - 'The minimum length is%d characters'=>'최소%d문자 필요합니다', - 'The password is required'=>'패스워드가 필요합니다', - 'This value must be an integer'=>'정수로 입력하세요', - 'The username must be unique'=>'사용자 이름이 이미 사용되고 있습니다', - 'The 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:'=>'애플리케이션의 버전:', - 'Completed on%B%e,%Y at%k:%M%p'=>'%Y/%m/%d%H:%M에 완료', - '%B%e,%Y at%k:%M%p'=>'%Y/%m/%d%H:%M', - 'Date created'=>'작성일', - 'Date completed'=>'완료일', - 'Id'=>'ID', - '%d closed tasks'=>'%d개의 마친 할일', - 'No task for this project'=>'이 프로젝트에 할일이 없습니다', - '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'=>'댓글', - 'Write your text in Markdown'=>'Markdown 사용', - 'Leave a comment'=>'댓글 남기기', - 'Comment is required'=>'댓글을 입력하세요', - 'Leave a description'=>'설명을 입력하세요', - 'Comment added successfully.'=>'의견을 추가했습니다.', - 'Unable to create your comment.'=>'댓글의 추가에 실패했습니다.', - 'Edit this task'=>'할일 수정', - 'Due Date'=>'마감일', - 'Invalid date'=>'날짜가 무효입니다', - 'Must be done before %B %e, %Y'=>'%Y-%m-%d %p %H:%M 까지 완료', - '%B%e,%Y'=>'%Y%B%e', - '%b%e,%Y'=>'%Y%b%e', - 'Automatic actions'=>'자동액션 관리', - 'Your automatic action have been created successfully.'=>'자동 액션을 작성했습니다.', - 'Unable to create your automatic action.'=>'자동 액션의 작성에 실패했습니다.', - 'Remove an action'=>'자동 액션의 삭제', - 'Unable to remove this action.'=>'자동 액션의 삭제에 실패했습니다.', - 'Action removed successfully.'=>'자동 액션의 삭제에 성공했어요.', - 'Automatic actions for the project"%s"'=>'프로젝트"%s"의 자동 액션', - 'Defined actions'=>'정의된 자동 액션', - 'Add an action'=>'자동 액션 추가', - 'Event name'=>'이벤트 이름', - 'Action name'=>'액션 이름', - 'Action parameters'=>'액션의 바로미터', - 'Action'=>'액션', - 'Event'=>'이벤트', - 'When the selected event occurs execute the corresponding action.'=>'선택된 이벤트가 발생했을 때 대응하는 액션을 실행한다.', - 'Next step'=>'다음 단계', - 'Define action parameters'=>'액션의 바로미터', - 'Save this action'=>'이 액션을 보존하는 ', - 'Do you really want to remove this action:"%s"('=>'자동 액션"%s"을 삭제할까요?', - 'Remove an automatic action'=>'자동 액션의 삭제', - 'Assign the task to a specific user'=>'할일 담당자를 할당', - 'Assign the task to the person who does the action'=>'액션을 일으킨 사용자를 담당자이자', - 'Duplicate the task to another project'=>' 다른 프로젝트에 할일을 복제하는 ', - 'Move a task to another column'=>'할일을 다른 칼럼에 이동하는 ', - 'Task modification'=>'할일 변경', - 'Task creation'=>'할일을 만들', - 'Closing a task'=>'할일을 닫혔다', - 'Assign a color to a specific user'=>'색을 사용자에 할당', - 'Column title'=>'칼럼의 제목', - 'Position'=>'위치', - 'Move Up'=>'위에 움직이는 ', - 'Move Down'=>'아래로 움직이는 ', - '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?'=>'댓글을 삭제합니까?', - 'Only administrators or the creator of the comment can access to this page.'=>'관리자나 의견 작성자만이 이 페이지 액세스 할 수 있습니다', - 'Current password for the user "%s"'=>'사용자 "%s"의 현재 패스워드', - 'The current password is required'=>'현재의 패스워드를 입력하세요', - 'Wrong password'=>'패스워드가 다릅니다', - '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'=>'이메일', - 'Link my Google Account'=>'Google계정을 연결하는 ', - 'Unlink my Google Account'=>'Google계정의 링크를 해제하는 ', - 'Login with my Google Account'=>'Google계정으로 로그인 한다', - 'Project not found.'=>'프로젝트가 발견되지 않습니다.', - 'Task removed successfully.'=>'할일을 삭제했습니다.', - 'Unable to remove this task.'=>'할일 삭제에 실패했습니다.', - 'Remove a task'=>'할일 삭제', - 'Do you really want to remove this task:"%s"('=>'할일"%s"을 삭제할까요?', - 'Assign automatically a color based on a category'=>'카테고리에 바탕을 두고 색을 바꾸고', - 'Assign automatically a category based on a color'=>'색에 바탕을 두고 카테고리를 바꾸었다', - 'Task creation or modification'=>'할일의 작성 또는 변경', - 'Category'=>'카테고리', - 'Category:'=>'카테고리:', - 'Categories'=>'카테고리', - 'Category not found.'=>'카테고리가 발견되지 않습니다', - 'Your category have been created successfully.'=>'카테고리를 작성했습니다.', - 'Unable to create your category.'=>'카테고리의 작성에 실패했습니다.', - 'Your category have been updated successfully.'=>'카테고리를 갱신했습니다.', - 'Unable to update your category.'=>'카테고리의 갱신에 실패했습니다.', - 'Remove a category'=>'카테고리의 삭제', - 'Category removed successfully.'=>'카테고리를 삭제했습니다.', - 'Unable to remove this category.'=>'카테고리를 삭제할 수 없었습니다.', - 'Category modification for the project"%s"'=>'프로젝트"%s"의 카테고리의 변경', - 'Category Name'=>'카테고리 이름', - 'Add a new category'=>'카테고리의 추가', - 'Do you really want to remove this category:"%s"('=>'카테고리"%s"을 삭제할까요?', - 'All categories'=>'모든 카테고리', - 'No category'=>'카테고리 없음', - 'The name is required'=>'이름을 입력하십시오', - 'Remove a file'=>'파일 삭제', - 'Unable to remove this file.'=>'파일 삭제에 실패했습니다.', - 'File removed successfully.'=>'파일을 삭제했습니다.', - 'Attach a document'=>'문서 첨부', - 'Do you really want to remove this file: "%s"?'=>'파일 "%s" 을 삭제할까요?', - 'Attachments'=>'첨부', - 'Edit the task'=>'할일 수정', - 'Edit the description'=>'설명 수정', - 'Add a comment'=>'댓글 추가', - 'Edit a comment'=>'댓글 수정', - 'Summary'=>'개요', - 'Time tracking'=>'시간 추적', - 'Estimate:'=>'예측:', - 'Spent:'=>'경과:', - 'Do you really want to remove this sub-task?'=>'서브 할일을 삭제합니까?', - 'Remaining:'=>'나머지:', - 'hours'=>'시간', - 'spent'=>'경과', - 'estimated'=>'예측', - 'Sub-Tasks'=>'서브 할일', - 'Add a sub-task'=>'서브 할일 추가', - 'Original estimate'=>'최초 예측시간', - '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'=>'프로젝트 보기', - 'Login with my Github Account'=>'Github계정으로 로그인 한다', - 'Link my Github Account'=>'Github계정을 연결하는 ', - 'Unlink my Github Account'=>'Github어카운트와의 링크를 해제하는 ', - 'Created by %s'=>'작성자 %s', - 'Last modified on %B %e, %Y at %k:%M %p'=>'%Y-%m-%d %p %H:%M 에 변경', - 'Tasks Export'=>'할일 내보내기', - 'Tasks exportation for"%s"'=>'"%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.'=>'할일#%d를 오픈했습니다.', - 'The task#%d have been closed.'=>'할일#%d을 닫혔습니다.', - 'Sub-task updated'=>'서브 할일 갱신', - 'Title:'=>'제목:', - 'Status:'=>'상태:', - 'Assignee:'=>'담당:', - 'Time tracking:'=>'시간 계측:', - 'New sub-task'=>'새로운 서브 할일', - 'New attachment added"%s"'=>'첨부 파일"%s"가 추가되었습니다', - 'Comment updated'=>'댓글가 갱신되었습니다', - 'New comment posted by %s'=>'"%s"님이 댓글을 추가하였습니다', - 'New attachment'=>' 새로운 첨부 파일', - 'New comment'=>' 새로운 댓글', - 'New subtask'=>' 새로운 서브 할일', - 'Subtask updated'=>'서브 할일 갱신', - 'Task updated'=>'할일 업데이트', - 'Task closed'=>'할일 마침', - 'Task opened'=>'할일 시작', - 'I want to receive notifications only for those projects:'=>'다음 프로젝트의 알림만 받겠습니다:', - 'view the task on Kanboard'=>'Kanboard에서 할일을 본다', - 'Public access'=>'공개 접속 설정', - 'User management'=>'사용자 관리', - 'Active tasks'=>'활성화된 할일', - 'Disable public access'=>'공개 접속 비활성화', - 'Enable public access'=>'공개 접속 활성화', - 'Public access disabled'=>'공개 접속 불가', - 'Do you really want to disable this project:"%s"('=>'"%s"를 무효로 합니까?', - 'Do you really want to enable this project:"%s"('=>'"%s"를 유효하게 합니까?', - 'Project activation'=>'프로젝트의 액티베ー션', - 'Move the task to another project'=>'할일별 프로젝트에 옮기', - 'Move to another project'=>'다른 프로젝트로 이동', - 'Do you really want to duplicate this task?'=>'할일을 복제합니까?', - 'Duplicate a task'=>'할일 복사', - 'External accounts'=>'외부 계정', - 'Account type'=>'계정종류', - 'Local'=>'로컬', - 'Remote'=>'원격', - 'Enabled'=>'활성화', - 'Disabled'=>'비활성화', - 'Username:'=>'사용자명', - 'Name:'=>'이름:', - 'Email:'=>'이메일:', - 'Notifications:'=>'알림:', - 'Notifications'=>'알림', - 'Account type:'=>'계정종류:', - 'Edit profile'=>'프로필 변경', - 'Change password'=>'패스워드 변경', - 'Password modification'=>'패스워드 변경', - 'External authentications'=>'외부 인증', - 'Google Account'=>'Google 계정', - 'Github Account'=>'Github 계정', - 'Never connected.'=>'접속기록없음', - 'No account linked.'=>'계정이 링크하지 않습니다.', - 'Account linked.'=>'계정이 링크했습니다.', - 'No external authentication enabled.'=>'외부 인증이 설정되어 있지 않습니다.', - 'Password modified successfully.'=>'패스워드를 변경했습니다.', - 'Unable to change the password.'=>'비밀 번호가 변경할 수 없었습니다.', - 'Change category for the task "%s"'=>'할일 "%s"의 카테고리의 변경', - 'Change category'=>'카테고리 수정', - '%s updated the task %s'=>'%s이 할일 %s을 업데이트했습니다', - '%s opened the task%s'=>'%s이 할일%s을 오픈했습니다', - '%s moved the task %s to the position #%d in the column "%s"'=>'%s이 할일%s을 위치#%d컬럼%s로 옮겼습니다', - '%s moved the task %s to the column "%s"'=>'%s이 할일 %s을 칼럼 "%s" 로 옮겼습니다', - '%s created the task %s'=>'%s이 할일%s을 추가했습니다', - '%s closed the task %s'=>'%s이 할일%s을 마쳤습니다', - '%s created a subtask for the task %s'=>'%s이 할일%s의 서브 할일을 추가했습니다', - '%s updated a subtask for the task %s'=>'%s이 할일%s의 서브 할일을 갱신했습니다', - 'Assigned to %s with an estimate of %s/%sh'=>'담당자 %s에게 예상 %s/%sh로 할당되었습니다', - 'Not assigned, estimate of%sh'=>'담당자 없이 예상%sh로 변경되었습니다', - '%s updated a comment on the task %s'=>'%s이 할일%s의 댓글을 수정했습니다', - '%s commented the task %s'=>'%s이 할일%s에 댓글을 남겼습니다', - '%s\'s activity'=>'%s의 활동', - 'RSS feed'=>'RSS피드', - '%s updated a comment on the task#%d'=>'%s이 할일#%d의 댓글을 갱신했습니다', - '%s commented on the task#%d'=>'%s이 할일#%d에 말했습니다', - '%s updated a subtask for the task#%d'=>'%s이 할일#%d의 서브 할일을 갱신했습니다', - '%s created a subtask for the task#%d'=>'%s이 할일#%d의 서브 할일을 추가했습니다', - '%s updated the task #%d'=>'%s이 할일#%d을 갱신했습니다', - '%s created the task #%d'=>'%s이 할일#%d을 추가했습니다', - '%s closed the task #%d'=>'%s이 할일#%d을 닫혔습니다', - '%s open the task #%d'=>'%s이 할일#%d를 오픈했습니다', - '%s moved the task #%d to the column "%s"'=>'%s이 할일#%d을 칼럼"%s"로 옮겼습니다', - '%s moved the task #%d to the position%d in the column "%s"'=>'%s이 할일#%d을 위치%d컬럼"%s"이동했습니다', - 'Activity'=>'활동', - 'Default values are"%s"'=>'기본 값은 "%s"', - 'Default columns for new projects(Comma-separated)'=>'신규 프로젝트의 기본 컬럼(쉼표로 구분하여 입력)', - 'Task assignee change'=>'담당자의 변경', - '%s change the assignee of the task#%d to%s'=>'%s이 할일#%d의 담당을%s로 변경했습니다', - '%s changed the assignee of the task %s to %s'=>'%s이 할일 %s의 담당을 %s로 변경했습니다', - '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'=>'참조', - 'Reference:%s'=>'참조:%s', - '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)'=>'할일이 최근 업데이트된 것으로 보면 기간(0은 하이라이트 무효 디폴트 2일)', - 'Frequency in second(60 seconds by default)'=>'초수(디폴트 60초)', - 'Frequency in second(0 to disable this feature, 10 seconds by default)'=>'초수(0은 기능을 무효화, 기본 10초)', - 'Application URL'=>'애플리케이션의 URL', - 'Example:http://example.kanboard.net/(used by email notifications)'=>'Exemple:http://exemple.kanboard.net/(Email통지에 이용)', - 'Token regenerated.'=>'토큰이 다시 생성되었습니다.', - 'Date format'=>'데이터 포맷', - 'ISO format is always accepted, example:"%s"and"%s"'=>'ISO포맷이 입력할 수 있습니다(예:%s또는%s)', - 'New private project'=>'새 비공개 프로젝트', - 'This project is private'=>'이 프로젝트는 비공개입니다', - 'Type here to create a new sub-task'=>'서브 할일을 추가하려면 여기에 입력하세요', - 'Add'=>'추가', - 'Estimated time: %s hours'=>'예상시간: %s시간', - 'Time spent: %s hours'=>'경과: %s시간', - 'Started on%B%e,%Y'=>'시작%Y/%m/%d', - 'Start date'=>'시작시간', - 'Time estimated'=>'예상시간', - 'There is nothing assigned to you.'=>'할일이 없습니다. 옆사람의 일을 도와주면 어떨까요?', - 'My tasks'=>'내 할일', - 'Activity stream'=>'활동기록', - 'Dashboard'=>'대시보드', - 'Confirmation'=>'확인', - 'Allow everybody to access to this project'=>'모든 사람이 이 프로젝트에 접근할 수 있도록 합니다', - 'Everybody have access to this project.'=>'누구나 이 프로젝트에 액세스 할 수 있습니다', - '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"'=>'"%s"의 할일 분포', - 'Analytics'=>'분석', - 'Subtask'=>'서브 할일', - 'My subtasks'=>'내 서브 할일', - 'User repartition'=>'담당자 분포', - 'User repartition for"%s"'=>'"%s"의 담당자 분포', - 'Clone this project'=>'이 프로젝트를 복제하는 ', - 'Column removed successfully.'=>'(※)컬럼을 삭제했습니다', - 'Not enough data to show the graph.'=>'그래프를 선묘화하려면 나왔지만 부족합니다', - 'Previous'=>' 돌아가', - 'The id must be an integer'=>'id은 숫자가 아니면 안 됩니다', - 'The project id must be an integer'=>'project id은 숫자가 아니면 안 됩니다', - 'The status must be an integer'=>'status는 숫자지 않으면 안 됩니다', - 'The subtask id is required'=>'subtask id가 필요합니다', - 'The subtask id must be an integer'=>'subtask id은 숫자가 아니면 안 됩니다', - 'The task id is required'=>'task id가 필요합니다', - 'The task id must be an integer'=>'task id은 숫자가 아니면 안 됩니다', - 'The user id must be an integer'=>'user id은 숫자가 아니면 안 됩니다', - 'This value is required'=>'이 값이 필요합니다', - 'This value must be numeric'=>'이 값은 숫자가 아니면 안 됩니다', - 'Unable to create this task.'=>'이 할일을 작성할 수 없었습니다', - 'Cumulative flow diagram'=>'축적 플로', - 'Cumulative flow diagram for"%s"'=>'"%s"의 축적 플로', - 'Daily project summary'=>'일시 프로젝트 개요', - 'Daily project summary export'=>'일시 프로젝트 개요의 출력', - 'Daily project summary export for"%s"'=>'"%s"의 일시 프로젝트 개요의 출력', - 'Exports'=>'출력', - 'This export contains the number of tasks per column grouped per day.'=>'이 출력은 날짜의 칼람별 할일 수를 집계한 것입니다', - 'Nothing to preview...'=>'미리보기가 없습니다', - 'Preview'=>'미리보기', - 'Write'=>'쓰기', - 'Active swimlanes'=>'액티브한 스윔레인', - 'Add a new swimlane'=>' 새로운 스윔레인', - 'Change default swimlane'=>'기본 스윔레인의 변경', - 'Default swimlane'=>'기본 스윔레인', - 'Do you really want to remove this swimlane:"%s"('=>'이 스윔레인"%s"를 정말로 삭제하시겠습니까?', - 'Inactive swimlanes'=>'인터랙티브한 스윔레인', - 'Remove a swimlane'=>'스윔레인의 삭제', - 'Rename'=>'이름 변경', - 'Show default swimlane'=>'기본 스윔레인의 표시', - 'Swimlane modification for the project"%s"'=>'"%s"에 대한 스윔레인 변경', - 'Swimlane not found.'=>'스윔레인이 발견되지 않습니다.', - 'Swimlane removed successfully.'=>'스윔레인을 삭제했습니다.', - 'Swimlanes'=>'스윔레인', - 'Swimlane updated successfully.'=>'스윔레인을 갱신했습니다.', - 'The default swimlane have been updated successfully.'=>'기본 스윔레인을 갱신했습니다.', - 'Unable to create your swimlane.'=>'스윔레인을 추가할 수 없었습니다.', - 'Unable to remove this swimlane.'=>'스윔레인을 삭제할 수 없었습니다.', - 'Unable to update this swimlane.'=>'스윔레인을 갱신할 수 없었습니다.', - 'Your swimlane have been created successfully.'=>'스윔레인이 작성되었습니다.', - 'Example:"Bug, Feature Request, Improvement"'=>'예:버그, 기능, 개선', - 'Default categories for new projects(Comma-separated)'=>' 새로운 프로젝트의 기본적 카테고리(쉼표 분리)', - 'Integrations'=>'연계', - 'Integration with third-party services'=>'외부 서비스 연계', - 'Subtask Id'=>'서브 할일 Id', - 'Subtasks'=>'서브 할일', - 'Subtasks Export'=>'서브 할일 출력', - 'Subtasks exportation for"%s"'=>'"%s"의 서브 할일 출력', - 'Task Title'=>'할일 제목', - 'Untitled'=>'제목 없음', - 'Application default'=>'애플리케이션 기본', - 'Language:'=>'언어:', - 'Timezone:'=>'시간대:', - 'All columns'=>'모든 칼럼', - 'Calendar'=>'달력', - 'Next'=>'다음에 ', - '#%d'=>'#%d', - 'All swimlanes'=>'모든 스윔레인', - 'All colors'=>'모든 색', - 'Moved to column%s'=>'칼럼%s로 이동했습니다', - 'Change description'=>'설명 수정', - 'User dashboard'=>'대시보드', - 'Allow only one subtask in progress at the same time for a user'=>'한 사용자에 대한 하나의 할일만 진행 중에 가능합니다', - 'Edit column"%s"'=>'칼럼"%s"의 편집', - 'Select the new status of the subtask:"%s"'=>'서브 할일"%s"의 위상을 선택', - 'Subtask timesheet'=>'서브 할일 타임시트', - 'There is nothing to show.'=>'기록이 없습니다', - 'Time Tracking'=>'타임 트레킹', - 'You already have one subtask in progress'=>'이미 진행 중인 서브 할일가 있습니다.', - 'Which parts of the project do you want to duplicate?'=>'프로젝트의 무엇을 복제합니까?', - //'Disallow login form'=>'', - 'Start'=>'시작', - 'End'=>'종료', - 'Task age in days'=>'할일이 생긴 시간', - 'Days in this column'=>'이 칼럼에 있는 시간', - '%dd'=>'%d일', - 'Add a link'=>'링크 추가', - 'Add a new link'=>' 새로운 링크 추가', - 'Do you really want to remove this link:"%s"('=>'링크"%s"를 정말로 삭제하시겠습니까?', - 'Do you really want to remove this link with task#%d?'=>'이 링크와 할일#%d을 삭제할까요?', - 'Field required'=>'필드가 필요합니다', - 'Link added successfully.'=>'링크를 추가했습니다.', - 'Link updated successfully.'=>'링크를 갱신했습니다.', - 'Link removed successfully.'=>'링크를 삭제했습니다.', - 'Link labels'=>'링크 라벨', - 'Link modification'=>'링크의 변경', - 'Links'=>'링크', - 'Link settings'=>'링크 설정', - 'Opposite label'=>'반대의 라벨', - 'Remove a link'=>'라벨의 삭제', - 'Task\'s links'=>'할일의 라벨', - 'The labels must be different'=>' 다른 라벨을 지정하세요', - 'There is no link.'=>'링크가 없습니다', - 'This label must be unique'=>'라벨은 독특할 필요가 있습니다', - 'Unable to create your link.'=>'링크를 작성할 수 없었습니다.', - 'Unable to update your link.'=>'링크를 갱신할 수 없었습니다.', - 'Unable to remove this link.'=>'링크를 삭제할 수 없었습니다.', - 'relates to'=>'연관 링크', - 'blocks'=>'다음을 딜레이하는', - 'is blocked by'=>'다음 때문에 딜레이되는', - 'duplicates'=>'다음과 중복하는', - 'is duplicated by'=>'다음에 중복되는', - 'is a child of'=>'다음의 하위 할일', - 'is a parent of'=>'다음의 상위 할일', - 'targets milestone'=>'다음의 이정표를 목표로 하는', - 'is a milestone of'=>'다음의 이정표인', - 'fixes'=>'다음을 수정하는', - 'is fixed by'=>'다음에 의해 수정되는', - 'This task'=>'이 할일의 ', - '<1h'=>'<1시간', - '%dh'=>'%d시간', - '%b%e'=>'%b/%e', - 'Expand tasks'=>'할일 크게', - 'Collapse tasks'=>'할일 작게', - 'Expand/collapse tasks'=>'할일 크게/작게', - 'Close dialog box'=>'다이얼로그를 닫습니다', - 'Submit a form'=>'제출', - 'Board view'=>'보드 뷰', - 'Keyboard shortcuts'=>'키보드 숏 컷', - 'Open board switcher'=>'보드 전환을 열', - 'Application'=>'애플리케이션', - 'since %B %e, %Y at %k:%M %p'=>'%Y-%m-%d %p %H:%M', - 'Compact view'=>'컴팩트 뷰', - 'Horizontal scrolling'=>'세로 스크롤', - 'Compact/wide view'=>'컴팩트/와이드 뷰', - 'No results match:'=>'결과가 일치하지 않았습니다', - 'Currency'=>'통화', - 'Files'=>'파일', - 'Images'=>'이미지', - 'Private project'=>'개인 프로젝트', - 'AUD-Australian Dollar'=>'AUD-호주 달러', - 'CAD-Canadian Dollar'=>'CAD-캐나다 달러', - 'CHF-Swiss Francs'=>'CHF-스위스 프랑', - 'Custom Stylesheet'=>'커스텀 스타일 시트', - 'download'=>'다운로드', - 'EUR-Euro'=>'EUR-유로', - 'GBP-British Pound'=>'GBP-독 파운드', - 'INR-Indian Rupee'=>'INR-이루피', - 'JPY-Japanese Yen'=>'JPY-일본 엔', - 'NZD-New Zealand Dollar'=>'NZD-NZ달러', - 'RSD-Serbian dinar'=>'RSD-세르비아 데나ー루', - 'USD-US Dollar'=>'USD-미국 달러', - 'Destination column'=>'이동 후 칼럼', - 'Move the task to another column when assigned to a user'=>'사용자의 할당을 하면 할일을 다른 칼럼에 이동', - 'Move the task to another column when assignee is cleared'=>'사용자의 할당이 없어지면 할일을 다른 칼럼에 이동', - '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:'=>'이 QR코드가 URI키를 포함하고 있습니다:', - 'Check my code'=>'코드 체크', - 'Secret key:'=>'비밀 키:', - 'Test your device'=>'디바이스 테스트', - - - + // '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', - 'uploaded by: %s' => '업로드: %s', - 'uploaded on: %s' => '날짜: %s', - 'size: %s' => '사이즈: %s', // 'Burndown chart for "%s"' => '', // 'Burndown chart' => '', // 'This chart show the task complexity over the time (Work Remaining).' => '', @@ -689,7 +634,6 @@ return array( 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '스크린샷을 CTRL+V 혹은 ⌘+V를 눌러 붙여넣기', 'Screenshot uploaded successfully.' => '스크린샷을 업로드하였습니다', // 'SEK - Swedish Krona' => '', - // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', // 'Identifier' => '', // 'Disable two factor authentication' => '', // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', @@ -698,7 +642,6 @@ return array( // 'A task cannot be linked to itself' => '', // 'The exact same link already exists' => '', // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', // 'Score' => '', // 'The identifier must be unique' => '', // 'This linked task id doesn\'t exists' => '', @@ -780,19 +723,13 @@ return array( 'Time spent changed: %sh' => '경과시간 변경: %s시간', 'Time estimated changed: %sh' => '%s시간으로 예상시간 변경', // 'The field "%s" have been updated' => '', - 'The description have been modified' => '설명 수정', + // 'The description has been modified:' => '', 'Do you really want to close the task "%s" as well as all subtasks?' => '할일 "%s"과 서브 할일을 모두 마치시겠습니까?', - // 'Swimlane: %s' => '', 'I want to receive notifications for:' => '다음의 알림을 받기를 원합니다:', 'All tasks' => '모든 할일', 'Only for tasks assigned to me' => '내가 담당자인 일', 'Only for tasks created by me' => '내가 만든 일', 'Only for tasks created by me and assigned to me' => '내가 만들었거나 내가 담당자인 일', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - 'New due date: %B %e, %Y' => '종료날짜 변경: %Y-%m-%d %p %H:%M', - 'Start date changed: %B %e, %Y' => '시작시간 변경: %Y-%m-%d %p %H:%M', - // '%k:%M %p' => '', // '%%Y-%%m-%%d' => '', // 'Total for all columns' => '', // 'You need at least 2 days of data to show the chart.' => '', @@ -805,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' => '오늘까지 할일', @@ -817,7 +752,6 @@ return array( 'Not assigned' => '담당자가 없는 일', 'View advanced search syntax' => '추가 검색 문법보기', 'Overview' => '개요', - // '%b %e %Y' => '', // 'Board/Calendar/List view' => '', // 'Switch to the board view' => '', // 'Switch to the calendar view' => '', @@ -850,10 +784,6 @@ return array( // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', // 'Average time into each column' => '', // 'Lead and cycle time' => '', - // 'Google Authentication' => '', - // 'Help on Google authentication' => '', - // 'Github Authentication' => '', - // 'Help on Github authentication' => '', 'Lead time: ' => '리드 타임: ', 'Cycle time: ' => '사이클 타임: ', 'Time spent into each column' => '각 칼럼에서 걸린 시간', @@ -862,16 +792,12 @@ return array( // 'If the task is not closed the current time is used instead of the completion date.' => '', // 'Set automatically the start date' => '', 'Edit Authentication' => '계정 수정', - // 'Google Id' => '', - // 'Github Id' => '', // 'Remote user' => '', // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', 'New remote user' => '새로운 원격유저', 'New local user' => '새로운 유저', // 'Default task color' => '', - 'Hide sidebar' => '사이드바 닫기', - 'Expand sidebar' => '사이드바 열기', 'This feature does not work with all browsers.' => '이 기능은 일부 브라우저에서 작동하지 않습니다', // 'There is no destination project available.' => '', // 'Trigger automatically subtask time tracking' => '', @@ -905,7 +831,6 @@ return array( 'open file' => '열기', 'End date' => '종료 날짜', 'Users overview' => '유저 전체보기', - 'Managers' => '매니저', 'Members' => '멤버', // 'Shared project' => '', 'Project managers' => '프로젝트 매니저', @@ -916,19 +841,9 @@ return array( // 'End date:' => '', 'There is no start date or end date for this project.' => '이 프로젝트에는 시작날짜와 종료날짜가 없습니다', 'Projects Gantt chart' => '프로젝트 간트차트', - // 'Start date: %s' => '', - // 'End date: %s' => '', - // 'Link type' => '', // 'Change task color when using a specific task link' => '', // 'Task link creation or modification' => '', - // 'Login with my Gitlab Account' => '', // 'Milestone' => '', - // 'Gitlab Authentication' => '', - // 'Help on Gitlab authentication' => '', - // 'Gitlab Id' => '', - // 'Gitlab Account' => '', - // 'Link my Gitlab Account' => '', - // 'Unlink my Gitlab Account' => '', // 'Documentation: %s' => '', // 'Switch to the Gantt chart view' => '', // 'Reset the search/filter box' => '', @@ -977,7 +892,6 @@ return array( // 'Shared' => '', // 'Owner' => '', 'Unread notifications' => '읽지않은 알림', - 'My filters' => '내 필터', 'Notification methods:' => '알림 방법', // 'Import tasks from CSV file' => '', // 'Unable to read your file' => '', @@ -1015,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' => '', @@ -1107,4 +1022,135 @@ return array( // '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' => '', ); diff --git a/app/Model/Config.php b/app/Model/Config.php index 3f9157ba..0c363fb0 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -90,7 +90,7 @@ class Config extends Setting 'fi_FI' => 'Suomi', 'sv_SE' => 'Svenska', 'tr_TR' => 'Türkçe', - 'ko_KR' => '한국어', + 'ko_KR' => '한국어', 'zh_CN' => '中文(简体)', 'ja_JP' => '日本語', 'th_TH' => 'ไทย', @@ -130,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', -- cgit v1.2.3 From d0a0be89f2a774e5438fe844d69541fecb5cafc4 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 18:30:02 -0400 Subject: Added more logging for LDAP client --- ChangeLog | 1 + app/Auth/LdapAuth.php | 6 +++++- app/Core/Ldap/Client.php | 33 +++++++++++++++++++++++++++++++++ app/Core/Ldap/Query.php | 6 ++++++ app/Core/Ldap/User.php | 3 +-- 5 files changed, 46 insertions(+), 3 deletions(-) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index c732f89a..4c79ef0c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -10,6 +10,7 @@ New features: Improvements: +* Added more logging for LDAP client * Improve schema migration process * Improve notification configuration form * Handle state in OAuth2 client 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/Core/Ldap/Client.php b/app/Core/Ldap/Client.php index 05658190..cee67da5 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 @@ -20,6 +21,14 @@ class Client */ protected $ldap; + /** + * Logger instance + * + * @access private + * @var LoggerInterface + */ + private $logger; + /** * Establish LDAP connection * @@ -165,4 +174,28 @@ 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; + } } diff --git a/app/Core/Ldap/Query.php b/app/Core/Ldap/Query.php index 1779fa61..bea6d5d6 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->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)); } -- cgit v1.2.3 From 74c674dac976664c6cd393a188644762d497752c Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 19:17:27 -0400 Subject: Update ChangeLog --- ChangeLog | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 4c79ef0c..bcfe30a6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,11 +1,13 @@ -Version 1.0.27 (unreleased) +Version 1.0.27 -------------- New features: * Added Markdown editor -* Added letter avatar provider -* Added pluggable Avatar providers +* Added Avatar with pluggable system + - Default is a letter based avatar + - Gravatar + - Avatar Image upload * Added Korean translation Improvements: -- cgit v1.2.3 From df964f9a929b803651b77f5965ada82112ce96cb Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 19:18:22 -0400 Subject: Fix typo --- ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index bcfe30a6..f07ba9e8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,7 +4,7 @@ Version 1.0.27 New features: * Added Markdown editor -* Added Avatar with pluggable system +* Added user avatars with pluggable system - Default is a letter based avatar - Gravatar - Avatar Image upload -- cgit v1.2.3 From 11858be4e8d5aba983700c6cba1c4d0a33ea8e9d Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 9 Apr 2016 22:42:17 -0400 Subject: Filter refactoring --- .travis.yml | 1 - ChangeLog | 7 + app/Controller/Analytic.php | 7 +- app/Controller/Board.php | 10 +- app/Controller/Calendar.php | 39 +- app/Controller/Gantt.php | 25 +- app/Controller/GroupHelper.php | 8 +- app/Controller/Ical.php | 59 +- app/Controller/Listing.php | 8 +- app/Controller/Search.php | 14 +- app/Controller/TaskHelper.php | 38 +- app/Controller/UserHelper.php | 13 +- app/Core/Action/ActionManager.php | 2 +- app/Core/Base.php | 30 +- app/Core/ExternalLink/ExternalLinkManager.php | 2 +- app/Core/Filter/CriteriaInterface.php | 40 ++ app/Core/Filter/FilterInterface.php | 56 ++ app/Core/Filter/FormatterInterface.php | 31 + app/Core/Filter/Lexer.php | 153 +++++ app/Core/Filter/LexerBuilder.php | 151 +++++ app/Core/Filter/OrCriteria.php | 68 ++ app/Core/Filter/QueryBuilder.php | 103 +++ app/Core/Helper.php | 2 + app/Core/Http/Response.php | 14 + app/Core/Lexer.php | 161 ----- app/Filter/BaseFilter.php | 119 ++++ app/Filter/ProjectGroupRoleProjectFilter.php | 38 ++ app/Filter/ProjectGroupRoleUsernameFilter.php | 44 ++ app/Filter/ProjectIdsFilter.php | 43 ++ app/Filter/ProjectStatusFilter.php | 45 ++ app/Filter/ProjectTypeFilter.php | 45 ++ app/Filter/ProjectUserRoleProjectFilter.php | 38 ++ app/Filter/ProjectUserRoleUsernameFilter.php | 41 ++ app/Filter/TaskAssigneeFilter.php | 75 +++ app/Filter/TaskCategoryFilter.php | 46 ++ app/Filter/TaskColorFilter.php | 60 ++ app/Filter/TaskColumnFilter.php | 44 ++ app/Filter/TaskCompletionDateFilter.php | 38 ++ app/Filter/TaskCreationDateFilter.php | 38 ++ app/Filter/TaskDescriptionFilter.php | 38 ++ app/Filter/TaskDueDateFilter.php | 41 ++ app/Filter/TaskDueDateRangeFilter.php | 39 ++ app/Filter/TaskIdExclusionFilter.php | 38 ++ app/Filter/TaskIdFilter.php | 38 ++ app/Filter/TaskLinkFilter.php | 85 +++ app/Filter/TaskModificationDateFilter.php | 38 ++ app/Filter/TaskProjectFilter.php | 44 ++ app/Filter/TaskProjectsFilter.php | 38 ++ app/Filter/TaskReferenceFilter.php | 38 ++ app/Filter/TaskStartDateFilter.php | 38 ++ app/Filter/TaskStatusFilter.php | 43 ++ app/Filter/TaskSubtaskAssigneeFilter.php | 140 ++++ app/Filter/TaskSwimlaneFilter.php | 50 ++ app/Filter/TaskTitleFilter.php | 46 ++ app/Filter/UserNameFilter.php | 35 + app/Formatter/BaseFormatter.php | 37 + app/Formatter/BaseTaskCalendarFormatter.php | 45 ++ app/Formatter/BoardFormatter.php | 56 ++ app/Formatter/FormatterInterface.php | 14 - app/Formatter/GroupAutoCompleteFormatter.php | 28 +- app/Formatter/ProjectGanttFormatter.php | 39 +- .../SubtaskTimeTrackingCalendarFormatter.php | 38 ++ app/Formatter/TaskAutoCompleteFormatter.php | 33 + app/Formatter/TaskCalendarFormatter.php | 74 ++ app/Formatter/TaskFilterAutoCompleteFormatter.php | 33 - app/Formatter/TaskFilterCalendarEvent.php | 76 --- app/Formatter/TaskFilterCalendarFormatter.php | 52 -- app/Formatter/TaskFilterGanttFormatter.php | 78 --- app/Formatter/TaskFilterICalendarFormatter.php | 133 ---- app/Formatter/TaskGanttFormatter.php | 78 +++ app/Formatter/TaskICalFormatter.php | 134 ++++ app/Formatter/UserAutoCompleteFormatter.php | 38 ++ app/Formatter/UserFilterAutoCompleteFormatter.php | 38 -- app/Helper/CalendarHelper.php | 112 ++++ app/Helper/ICalHelper.php | 38 ++ app/Model/AvatarFile.php | 1 + app/Model/Base.php | 24 - app/Model/Project.php | 14 + app/Model/ProjectActivity.php | 6 +- app/Model/ProjectGroupRoleFilter.php | 89 --- app/Model/ProjectPermission.php | 18 +- app/Model/ProjectUserRole.php | 4 +- app/Model/ProjectUserRoleFilter.php | 88 --- app/Model/Setting.php | 1 + app/Model/SubtaskTimeTracking.php | 88 --- app/Model/TaskFilter.php | 745 --------------------- app/Model/TaskFinder.php | 21 + app/Model/UserFilter.php | 80 --- app/ServiceProvider/ClassProvider.php | 12 - app/ServiceProvider/FilterProvider.php | 112 ++++ app/ServiceProvider/HelperProvider.php | 2 + app/common.php | 1 + composer.lock | 20 +- doc/installation.markdown | 2 +- doc/plugin-hooks.markdown | 9 - doc/update.markdown | 2 +- tests/units/Base.php | 1 + tests/units/Core/Filter/LexerBuilderTest.php | 106 +++ tests/units/Core/Filter/LexerTest.php | 100 +++ tests/units/Core/Filter/OrCriteriaTest.php | 58 ++ tests/units/Core/LexerTest.php | 468 ------------- tests/units/Filter/TaskAssigneeFilterTest.php | 159 +++++ .../Formatter/TaskFilterCalendarFormatterTest.php | 21 - .../Formatter/TaskFilterGanttFormatterTest.php | 24 - .../Formatter/TaskFilterICalendarFormatterTest.php | 74 -- tests/units/Model/SubtaskTimeTrackingTest.php | 77 --- tests/units/Model/TaskFilterTest.php | 624 ----------------- 107 files changed, 3582 insertions(+), 3188 deletions(-) create mode 100644 app/Core/Filter/CriteriaInterface.php create mode 100644 app/Core/Filter/FilterInterface.php create mode 100644 app/Core/Filter/FormatterInterface.php create mode 100644 app/Core/Filter/Lexer.php create mode 100644 app/Core/Filter/LexerBuilder.php create mode 100644 app/Core/Filter/OrCriteria.php create mode 100644 app/Core/Filter/QueryBuilder.php delete mode 100644 app/Core/Lexer.php create mode 100644 app/Filter/BaseFilter.php create mode 100644 app/Filter/ProjectGroupRoleProjectFilter.php create mode 100644 app/Filter/ProjectGroupRoleUsernameFilter.php create mode 100644 app/Filter/ProjectIdsFilter.php create mode 100644 app/Filter/ProjectStatusFilter.php create mode 100644 app/Filter/ProjectTypeFilter.php create mode 100644 app/Filter/ProjectUserRoleProjectFilter.php create mode 100644 app/Filter/ProjectUserRoleUsernameFilter.php create mode 100644 app/Filter/TaskAssigneeFilter.php create mode 100644 app/Filter/TaskCategoryFilter.php create mode 100644 app/Filter/TaskColorFilter.php create mode 100644 app/Filter/TaskColumnFilter.php create mode 100644 app/Filter/TaskCompletionDateFilter.php create mode 100644 app/Filter/TaskCreationDateFilter.php create mode 100644 app/Filter/TaskDescriptionFilter.php create mode 100644 app/Filter/TaskDueDateFilter.php create mode 100644 app/Filter/TaskDueDateRangeFilter.php create mode 100644 app/Filter/TaskIdExclusionFilter.php create mode 100644 app/Filter/TaskIdFilter.php create mode 100644 app/Filter/TaskLinkFilter.php create mode 100644 app/Filter/TaskModificationDateFilter.php create mode 100644 app/Filter/TaskProjectFilter.php create mode 100644 app/Filter/TaskProjectsFilter.php create mode 100644 app/Filter/TaskReferenceFilter.php create mode 100644 app/Filter/TaskStartDateFilter.php create mode 100644 app/Filter/TaskStatusFilter.php create mode 100644 app/Filter/TaskSubtaskAssigneeFilter.php create mode 100644 app/Filter/TaskSwimlaneFilter.php create mode 100644 app/Filter/TaskTitleFilter.php create mode 100644 app/Filter/UserNameFilter.php create mode 100644 app/Formatter/BaseFormatter.php create mode 100644 app/Formatter/BaseTaskCalendarFormatter.php create mode 100644 app/Formatter/BoardFormatter.php delete mode 100644 app/Formatter/FormatterInterface.php create mode 100644 app/Formatter/SubtaskTimeTrackingCalendarFormatter.php create mode 100644 app/Formatter/TaskAutoCompleteFormatter.php create mode 100644 app/Formatter/TaskCalendarFormatter.php delete mode 100644 app/Formatter/TaskFilterAutoCompleteFormatter.php delete mode 100644 app/Formatter/TaskFilterCalendarEvent.php delete mode 100644 app/Formatter/TaskFilterCalendarFormatter.php delete mode 100644 app/Formatter/TaskFilterGanttFormatter.php delete mode 100644 app/Formatter/TaskFilterICalendarFormatter.php create mode 100644 app/Formatter/TaskGanttFormatter.php create mode 100644 app/Formatter/TaskICalFormatter.php create mode 100644 app/Formatter/UserAutoCompleteFormatter.php delete mode 100644 app/Formatter/UserFilterAutoCompleteFormatter.php create mode 100644 app/Helper/CalendarHelper.php create mode 100644 app/Helper/ICalHelper.php delete mode 100644 app/Model/ProjectGroupRoleFilter.php delete mode 100644 app/Model/ProjectUserRoleFilter.php delete mode 100644 app/Model/TaskFilter.php delete mode 100644 app/Model/UserFilter.php create mode 100644 app/ServiceProvider/FilterProvider.php create mode 100644 tests/units/Core/Filter/LexerBuilderTest.php create mode 100644 tests/units/Core/Filter/LexerTest.php create mode 100644 tests/units/Core/Filter/OrCriteriaTest.php delete mode 100644 tests/units/Core/LexerTest.php create mode 100644 tests/units/Filter/TaskAssigneeFilterTest.php delete mode 100644 tests/units/Formatter/TaskFilterCalendarFormatterTest.php delete mode 100644 tests/units/Formatter/TaskFilterGanttFormatterTest.php delete mode 100644 tests/units/Formatter/TaskFilterICalendarFormatterTest.php delete mode 100644 tests/units/Model/TaskFilterTest.php (limited to 'ChangeLog') diff --git a/.travis.yml b/.travis.yml index 1c132a0b..40af3ca8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,6 @@ before_script: - phpenv config-rm xdebug.ini - phpenv config-add tests/php.ini - composer install - - php -i script: - phpunit -c tests/units.$DB.xml diff --git a/ChangeLog b/ChangeLog index f07ba9e8..ea12d9b9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +Version 1.0.28 (unreleased) +-------------- + +Improvements: + +* Filter/Lexer/QueryBuilder refactoring + Version 1.0.27 -------------- diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php index 6b0730b0..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,13 +45,15 @@ class Analytic extends Base public function compareHours() { $project = $this->getProject(); - $query = $this->taskFilter->create()->filterByProject($project['id'])->getQuery(); $paginator = $this->paginator ->setUrl('analytic', 'compareHours', array('project_id' => $project['id'])) ->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/Board.php b/app/Controller/Board.php index 51344bd3..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 * @@ -51,12 +53,14 @@ class Board extends Base $search = $this->helper->projectHeader->getSearchQuery($project); $this->response->html($this->helper->layout->app('board/view_private', array( - 'swimlanes' => $this->taskFilter->search($search)->getBoard($project['id']), 'project' => $project, 'title' => $project['name'], 'description' => $this->helper->projectHeader->getDescription($project), 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), 'board_highlight_period' => $this->config->get('board_highlight_period'), + 'swimlanes' => $this->taskLexer + ->build($search) + ->format(BoardFormatter::getInstance($this->container)->setProjectId($project['id'])) ))); } @@ -178,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 af31ae47..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; /** @@ -40,21 +43,11 @@ class Calendar extends Base $project_id = $this->request->getIntegerParam('project_id'); $start = $this->request->getStringParam('start'); $end = $this->request->getStringParam('end'); + $search = $this->userSession->getFilters($project_id); + $queryBuilder = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project_id)); - // Common filter - $filter = $this->taskFilterCalendarFormatter - ->search($this->userSession->getFilters($project_id)) - ->filterByProject($project_id); - - // Tasks - if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { - $events = $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format(); - } else { - $events = $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format(); - } - - // Tasks with due date - $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format()); + $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end); + $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end)); $events = $this->hook->merge('controller:calendar:project:events', $events, array( 'project_id' => $project_id, @@ -75,21 +68,15 @@ class Calendar extends Base $user_id = $this->request->getIntegerParam('user_id'); $start = $this->request->getStringParam('start'); $end = $this->request->getStringParam('end'); - $filter = $this->taskFilterCalendarFormatter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN); + $queryBuilder = $this->taskQuery + ->withFilter(new TaskAssigneeFilter($user_id)) + ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN)); - // Task with due date - $events = $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format(); - - // 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/Gantt.php b/app/Controller/Gantt.php index 02ee946c..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'), ))); } @@ -56,8 +65,8 @@ class Gantt extends Base { $project = $this->getProject(); $search = $this->helper->projectHeader->getSearchQuery($project); - $filter = $this->taskFilterGanttFormatter->search($search)->filterByProject($project['id']); $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'); @@ -70,7 +79,7 @@ class Gantt extends Base '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 9931c346..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; /** @@ -21,14 +22,17 @@ class Listing extends Base { $project = $this->getProject(); $search = $this->helper->projectHeader->getSearchQuery($project); - $query = $this->taskFilter->search($search)->filterByProject($project['id'])->getQuery(); $paginator = $this->paginator ->setUrl('listing', 'show', array('project_id' => $project['id'])) ->setMax(30) ->setOrder(TaskModel::TABLE.'.id') ->setDirection('DESC') - ->setQuery($query) + ->setQuery($this->taskLexer + ->build($search) + ->withFilter(new TaskProjectFilter($project['id'])) + ->getQuery() + ) ->calculate(); $this->response->html($this->helper->layout->app('listing/show', array( diff --git a/app/Controller/Search.php b/app/Controller/Search.php index 9b9b9e65..840a90c8 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(); 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 74573e94..8c6b7620 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -48,16 +48,8 @@ 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 @@ -85,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 @@ -99,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 @@ -137,6 +127,12 @@ 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 $userQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery + * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery + * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher @@ -173,4 +169,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/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 @@ + '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 @@ +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 @@ +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 @@ +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 3a66fbd0..ab1f8f76 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -12,10 +12,12 @@ use Pimple\Container; * * @property \Kanboard\Helper\AppHelper $app * @property \Kanboard\Helper\AssetHelper $asset + * @property \Kanboard\Helper\CalendarHelper $calendar * @property \Kanboard\Helper\DateHelper $dt * @property \Kanboard\Helper\FileHelper $file * @property \Kanboard\Helper\FormHelper $form * @property \Kanboard\Helper\HookHelper $hook + * @property \Kanboard\Helper\ICalHelper $ical * @property \Kanboard\Helper\ModelHelper $model * @property \Kanboard\Helper\SubtaskHelper $subtask * @property \Kanboard\Helper\TaskHelper $task diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index 37349ca5..996fc58d 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -231,6 +231,20 @@ class Response extends Base exit; } + /** + * 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 * 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 @@ - '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/Filter/BaseFilter.php b/app/Filter/BaseFilter.php new file mode 100644 index 00000000..a7e6a61a --- /dev/null +++ b/app/Filter/BaseFilter.php @@ -0,0 +1,119 @@ +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; + } + + /** + * 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) + { + $timestamp = strtotime($this->value); + $method = $this->parseOperator(); + + if ($method !== '') { + $this->query->$method($field, $timestamp); + } else { + $this->query->gte($field, $timestamp); + $this->query->lte($field, $timestamp + 86399); + } + } +} 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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/TaskCompletionDateFilter.php b/app/Filter/TaskCompletionDateFilter.php new file mode 100644 index 00000000..5166bebf --- /dev/null +++ b/app/Filter/TaskCompletionDateFilter.php @@ -0,0 +1,38 @@ +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..26318b3e --- /dev/null +++ b/app/Filter/TaskCreationDateFilter.php @@ -0,0 +1,38 @@ +applyDateFilter(Task::TABLE.'.date_creation'); + return $this; + } +} 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 @@ +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..6ba55eb9 --- /dev/null +++ b/app/Filter/TaskDueDateFilter.php @@ -0,0 +1,41 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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..d8838bce --- /dev/null +++ b/app/Filter/TaskModificationDateFilter.php @@ -0,0 +1,38 @@ +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 @@ +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..e0fc09cf --- /dev/null +++ b/app/Filter/TaskProjectsFilter.php @@ -0,0 +1,38 @@ +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 @@ +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..d45bc0d4 --- /dev/null +++ b/app/Filter/TaskStartDateFilter.php @@ -0,0 +1,38 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +query = $query; + return $this; + } +} diff --git a/app/Formatter/BaseTaskCalendarFormatter.php b/app/Formatter/BaseTaskCalendarFormatter.php new file mode 100644 index 00000000..8fab3e9a --- /dev/null +++ b/app/Formatter/BaseTaskCalendarFormatter.php @@ -0,0 +1,45 @@ +startColumn = $start_column; + $this->endColumn = $end_column ?: $start_column; + return $this; + } +} 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 @@ +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 @@ -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/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,40 +10,8 @@ use Kanboard\Model\Project; * @package formatter * @author Frederic Guillot */ -class ProjectGanttFormatter extends Project implements FormatterInterface +class ProjectGanttFormatter extends BaseFormatter implements FormatterInterface { - /** - * List of projects - * - * @access private - * @var array - */ - private $projects = array(); - - /** - * Filter projects to generate the Gantt chart - * - * @access public - * @param int[] $project_ids - * @return ProjectGanttFormatter - */ - public function filter(array $project_ids) - { - if (empty($project_ids)) { - $this->projects = array(); - } else { - $this->projects = $this->db - ->table(self::TABLE) - ->asc('start_date') - ->in('id', $project_ids) - ->eq('is_active', self::ACTIVE) - ->eq('is_private', 0) - ->findAll(); - } - - return $this; - } - /** * Format projects to be displayed in the Gantt chart * @@ -52,10 +20,11 @@ class ProjectGanttFormatter extends Project implements FormatterInterface */ public function format() { + $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 @@ +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/TaskAutoCompleteFormatter.php b/app/Formatter/TaskAutoCompleteFormatter.php new file mode 100644 index 00000000..480ee797 --- /dev/null +++ b/app/Formatter/TaskAutoCompleteFormatter.php @@ -0,0 +1,33 @@ +query->columns(Task::TABLE.'.id', Task::TABLE.'.title')->findAll(); + + foreach ($tasks as &$task) { + $task['value'] = $task['title']; + $task['label'] = '#'.$task['id'].' - '.$task['title']; + } + + return $tasks; + } +} diff --git a/app/Formatter/TaskCalendarFormatter.php b/app/Formatter/TaskCalendarFormatter.php new file mode 100644 index 00000000..60b9a062 --- /dev/null +++ b/app/Formatter/TaskCalendarFormatter.php @@ -0,0 +1,74 @@ +fullDay = true; + return $this; + } + + /** + * Transform tasks to calendar events + * + * @access public + * @return array + */ + public function format() + { + $events = array(); + + foreach ($this->query->findAll() as $task) { + $events[] = array( + 'timezoneParam' => $this->config->getCurrentTimezone(), + 'id' => $task['id'], + 'title' => t('#%d', $task['id']).' '.$task['title'], + 'backgroundColor' => $this->color->getBackgroundColor($task['color_id']), + 'borderColor' => $this->color->getBorderColor($task['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + 'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]), + 'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()), + 'editable' => $this->fullDay, + 'allday' => $this->fullDay, + ); + } + + return $events; + } + + /** + * Get DateTime format for event + * + * @access private + * @return string + */ + private function getDateTimeFormat() + { + return $this->fullDay ? 'Y-m-d' : 'Y-m-d\TH:i:s'; + } +} diff --git a/app/Formatter/TaskFilterAutoCompleteFormatter.php b/app/Formatter/TaskFilterAutoCompleteFormatter.php deleted file mode 100644 index c9af4654..00000000 --- a/app/Formatter/TaskFilterAutoCompleteFormatter.php +++ /dev/null @@ -1,33 +0,0 @@ -query->columns(Task::TABLE.'.id', Task::TABLE.'.title')->findAll(); - - foreach ($tasks as &$task) { - $task['value'] = $task['title']; - $task['label'] = '#'.$task['id'].' - '.$task['title']; - } - - return $tasks; - } -} diff --git a/app/Formatter/TaskFilterCalendarEvent.php b/app/Formatter/TaskFilterCalendarEvent.php deleted file mode 100644 index 12ea8687..00000000 --- a/app/Formatter/TaskFilterCalendarEvent.php +++ /dev/null @@ -1,76 +0,0 @@ -startColumn = $start_column; - $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/TaskFilterCalendarFormatter.php b/app/Formatter/TaskFilterCalendarFormatter.php deleted file mode 100644 index 1b5d6ca4..00000000 --- a/app/Formatter/TaskFilterCalendarFormatter.php +++ /dev/null @@ -1,52 +0,0 @@ -query->findAll() as $task) { - $events[] = array( - 'timezoneParam' => $this->config->getCurrentTimezone(), - 'id' => $task['id'], - 'title' => t('#%d', $task['id']).' '.$task['title'], - 'backgroundColor' => $this->color->getBackgroundColor($task['color_id']), - 'borderColor' => $this->color->getBorderColor($task['color_id']), - 'textColor' => 'black', - 'url' => $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), - 'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]), - 'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()), - 'editable' => $this->isFullDay(), - 'allday' => $this->isFullDay(), - ); - } - - return $events; - } - - /** - * Get DateTime format for event - * - * @access private - * @return string - */ - private function getDateTimeFormat() - { - return $this->isFullDay() ? 'Y-m-d' : 'Y-m-d\TH:i:s'; - } -} diff --git a/app/Formatter/TaskFilterGanttFormatter.php b/app/Formatter/TaskFilterGanttFormatter.php deleted file mode 100644 index a4eef1ee..00000000 --- a/app/Formatter/TaskFilterGanttFormatter.php +++ /dev/null @@ -1,78 +0,0 @@ -query->findAll() as $task) { - $bars[] = $this->formatTask($task); - } - - return $bars; - } - - /** - * Format a single task - * - * @access private - * @param array $task - * @return array - */ - private function formatTask(array $task) - { - if (! isset($this->columns[$task['project_id']])) { - $this->columns[$task['project_id']] = $this->column->getList($task['project_id']); - } - - $start = $task['date_started'] ?: time(); - $end = $task['date_due'] ?: $start; - - return array( - 'type' => 'task', - 'id' => $task['id'], - 'title' => $task['title'], - 'start' => array( - (int) date('Y', $start), - (int) date('n', $start), - (int) date('j', $start), - ), - 'end' => array( - (int) date('Y', $end), - (int) date('n', $end), - (int) date('j', $end), - ), - 'column_title' => $task['column_name'], - 'assignee' => $task['assignee_name'] ?: $task['assignee_username'], - 'progress' => $this->task->getProgress($task, $this->columns[$task['project_id']]).'%', - 'link' => $this->helper->url->href('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), - 'color' => $this->color->getColorProperties($task['color_id']), - 'not_defined' => empty($task['date_due']) || empty($task['date_started']), - ); - } -} diff --git a/app/Formatter/TaskFilterICalendarFormatter.php b/app/Formatter/TaskFilterICalendarFormatter.php deleted file mode 100644 index 25b3aea0..00000000 --- a/app/Formatter/TaskFilterICalendarFormatter.php +++ /dev/null @@ -1,133 +0,0 @@ -vCalendar->render(); - } - - /** - * Set calendar object - * - * @access public - * @param \Eluceo\iCal\Component\Calendar $vCalendar - * @return TaskFilterICalendarFormatter - */ - public function setCalendar(Calendar $vCalendar) - { - $this->vCalendar = $vCalendar; - return $this; - } - - /** - * Transform results to ical events - * - * @access public - * @return TaskFilterICalendarFormatter - */ - public function addDateTimeEvents() - { - foreach ($this->query->findAll() as $task) { - $start = new DateTime; - $start->setTimestamp($task[$this->startColumn]); - - $end = new DateTime; - $end->setTimestamp($task[$this->endColumn] ?: time()); - - $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$this->startColumn.'-'.$this->endColumn); - $vEvent->setDtStart($start); - $vEvent->setDtEnd($end); - - $this->vCalendar->addComponent($vEvent); - } - - return $this; - } - - /** - * Transform results to all day ical events - * - * @access public - * @return TaskFilterICalendarFormatter - */ - public function addFullDayEvents() - { - foreach ($this->query->findAll() as $task) { - $date = new DateTime; - $date->setTimestamp($task[$this->startColumn]); - - $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$this->startColumn); - $vEvent->setDtStart($date); - $vEvent->setDtEnd($date); - $vEvent->setNoTime(true); - - $this->vCalendar->addComponent($vEvent); - } - - return $this; - } - - /** - * Get common events for task ical events - * - * @access protected - * @param array $task - * @param string $uid - * @return Event - */ - protected function getTaskIcalEvent(array &$task, $uid) - { - $dateCreation = new DateTime; - $dateCreation->setTimestamp($task['date_creation']); - - $dateModif = new DateTime; - $dateModif->setTimestamp($task['date_modification']); - - $vEvent = new Event($uid); - $vEvent->setCreated($dateCreation); - $vEvent->setModified($dateModif); - $vEvent->setUseTimezone(true); - $vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']); - $vEvent->setUrl($this->helper->url->base().$this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - - if (! empty($task['owner_id'])) { - $vEvent->setOrganizer($task['assignee_name'] ?: $task['assignee_username'], $task['assignee_email']); - } - - if (! empty($task['creator_id'])) { - $attendees = new Attendees; - $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); - $vEvent->setAttendees($attendees); - } - - return $vEvent; - } -} diff --git a/app/Formatter/TaskGanttFormatter.php b/app/Formatter/TaskGanttFormatter.php new file mode 100644 index 00000000..3209aa37 --- /dev/null +++ b/app/Formatter/TaskGanttFormatter.php @@ -0,0 +1,78 @@ +query->findAll() as $task) { + $bars[] = $this->formatTask($task); + } + + return $bars; + } + + /** + * Format a single task + * + * @access private + * @param array $task + * @return array + */ + private function formatTask(array $task) + { + if (! isset($this->columns[$task['project_id']])) { + $this->columns[$task['project_id']] = $this->column->getList($task['project_id']); + } + + $start = $task['date_started'] ?: time(); + $end = $task['date_due'] ?: $start; + + return array( + 'type' => 'task', + 'id' => $task['id'], + 'title' => $task['title'], + 'start' => array( + (int) date('Y', $start), + (int) date('n', $start), + (int) date('j', $start), + ), + 'end' => array( + (int) date('Y', $end), + (int) date('n', $end), + (int) date('j', $end), + ), + 'column_title' => $task['column_name'], + 'assignee' => $task['assignee_name'] ?: $task['assignee_username'], + 'progress' => $this->task->getProgress($task, $this->columns[$task['project_id']]).'%', + 'link' => $this->helper->url->href('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), + 'color' => $this->color->getColorProperties($task['color_id']), + 'not_defined' => empty($task['date_due']) || empty($task['date_started']), + ); + } +} diff --git a/app/Formatter/TaskICalFormatter.php b/app/Formatter/TaskICalFormatter.php new file mode 100644 index 00000000..a149f725 --- /dev/null +++ b/app/Formatter/TaskICalFormatter.php @@ -0,0 +1,134 @@ +vCalendar->render(); + } + + /** + * Set calendar object + * + * @access public + * @param \Eluceo\iCal\Component\Calendar $vCalendar + * @return FormatterInterface + */ + public function setCalendar(Calendar $vCalendar) + { + $this->vCalendar = $vCalendar; + return $this; + } + + /** + * Transform results to iCal events + * + * @access public + * @return FormatterInterface + */ + public function addDateTimeEvents() + { + foreach ($this->query->findAll() as $task) { + $start = new DateTime; + $start->setTimestamp($task[$this->startColumn]); + + $end = new DateTime; + $end->setTimestamp($task[$this->endColumn] ?: time()); + + $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$this->startColumn.'-'.$this->endColumn); + $vEvent->setDtStart($start); + $vEvent->setDtEnd($end); + + $this->vCalendar->addComponent($vEvent); + } + + return $this; + } + + /** + * Transform results to all day iCal events + * + * @access public + * @return FormatterInterface + */ + public function addFullDayEvents() + { + foreach ($this->query->findAll() as $task) { + $date = new DateTime; + $date->setTimestamp($task[$this->startColumn]); + + $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$this->startColumn); + $vEvent->setDtStart($date); + $vEvent->setDtEnd($date); + $vEvent->setNoTime(true); + + $this->vCalendar->addComponent($vEvent); + } + + return $this; + } + + /** + * Get common events for task iCal events + * + * @access protected + * @param array $task + * @param string $uid + * @return Event + */ + protected function getTaskIcalEvent(array &$task, $uid) + { + $dateCreation = new DateTime; + $dateCreation->setTimestamp($task['date_creation']); + + $dateModif = new DateTime; + $dateModif->setTimestamp($task['date_modification']); + + $vEvent = new Event($uid); + $vEvent->setCreated($dateCreation); + $vEvent->setModified($dateModif); + $vEvent->setUseTimezone(true); + $vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']); + $vEvent->setUrl($this->helper->url->base().$this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + + if (! empty($task['owner_id'])) { + $vEvent->setOrganizer($task['assignee_name'] ?: $task['assignee_username'], $task['assignee_email']); + } + + if (! empty($task['creator_id'])) { + $attendees = new Attendees; + $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); + $vEvent->setAttendees($attendees); + } + + return $vEvent; + } +} diff --git a/app/Formatter/UserAutoCompleteFormatter.php b/app/Formatter/UserAutoCompleteFormatter.php new file mode 100644 index 00000000..c46a24d0 --- /dev/null +++ b/app/Formatter/UserAutoCompleteFormatter.php @@ -0,0 +1,38 @@ +query->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')->findAll(); + + foreach ($users as &$user) { + $user['value'] = $user['username'].' (#'.$user['id'].')'; + + if (empty($user['name'])) { + $user['label'] = $user['username']; + } else { + $user['label'] = $user['name'].' ('.$user['username'].')'; + } + } + + return $users; + } +} diff --git a/app/Formatter/UserFilterAutoCompleteFormatter.php b/app/Formatter/UserFilterAutoCompleteFormatter.php deleted file mode 100644 index b98e0d69..00000000 --- a/app/Formatter/UserFilterAutoCompleteFormatter.php +++ /dev/null @@ -1,38 +0,0 @@ -query->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name')->findAll(); - - foreach ($users as &$user) { - $user['value'] = $user['username'].' (#'.$user['id'].')'; - - if (empty($user['name'])) { - $user['label'] = $user['username']; - } else { - $user['label'] = $user['name'].' ('.$user['username'].')'; - } - } - - return $users; - } -} diff --git a/app/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 @@ +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 @@ +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/Model/AvatarFile.php b/app/Model/AvatarFile.php index 52d07962..c49f9fd5 100644 --- a/app/Model/AvatarFile.php +++ b/app/Model/AvatarFile.php @@ -76,6 +76,7 @@ class AvatarFile extends Base * @access public * @param integer $user_id * @param array $file + * @return boolean */ public function uploadFile($user_id, array $file) { 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/Project.php b/app/Model/Project.php index d2e5b7ce..6e3c2326 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -34,6 +34,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 * diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index d399d5c6..34893f0b 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -2,6 +2,8 @@ namespace Kanboard\Model; +use PicoDb\Table; + /** * Project activity model * @@ -133,12 +135,12 @@ class ProjectActivity extends Base * Common function to return events * * @access public - * @param \PicoDb\Table $query PicoDb Query + * @param 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) + private function getEvents(Table $query, $start, $end) { if (! is_null($start)) { $query->gte('date_creation', $start); 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 @@ -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 @@ -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 @@ -145,94 +145,6 @@ class SubtaskTimeTracking extends Base ->findAll(); } - /** - * Get user calendar events - * - * @access public - * @param integer $user_id - * @param string $start ISO-8601 format - * @param string $end - * @return array - */ - public function getUserCalendarEvents($user_id, $start, $end) - { - $hook = 'model:subtask-time-tracking:calendar:events'; - $events = $this->getUserQuery($user_id) - ->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'start', - 'end' - )) - ->findAll(); - - if ($this->hook->exists($hook)) { - $events = $this->hook->first($hook, array( - 'user_id' => $user_id, - 'events' => $events, - 'start' => $start, - 'end' => $end, - )); - } - - return $this->toCalendarEvents($events); - } - - /** - * Get project calendar events - * - * @access public - * @param integer $project_id - * @param integer $start - * @param integer $end - * @return array - */ - public function getProjectCalendarEvents($project_id, $start, $end) - { - $result = $this - ->getProjectQuery($project_id) - ->addCondition($this->getCalendarCondition( - $this->dateParser->getTimestampFromIsoFormat($start), - $this->dateParser->getTimestampFromIsoFormat($end), - 'start', - 'end' - )) - ->findAll(); - - return $this->toCalendarEvents($result); - } - - /** - * Convert a record set to calendar events - * - * @access private - * @param array $rows - * @return array - */ - private function toCalendarEvents(array $rows) - { - $events = array(); - - foreach ($rows as $row) { - $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : ''; - - $events[] = array( - 'id' => $row['id'], - 'subtask_id' => $row['subtask_id'], - 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user, - 'start' => date('Y-m-d\TH:i:s', $row['start']), - 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()), - 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']), - 'borderColor' => $this->color->getBorderColor($row['color_id']), - 'textColor' => 'black', - 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])), - 'editable' => false, - ); - } - - return $events; - } - /** * Return true if a timer is started for this use and subtask * 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 @@ - '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 7bca2284..1840b505 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -362,6 +362,27 @@ class TaskFinder extends Base return $rq->fetch(PDO::FETCH_ASSOC); } + /** + * Get iCal query + * + * @access public + * @return \PicoDb\Table + */ + public function getICalQuery() + { + return $this->db->table(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 * 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 @@ -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/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 3e654a4e..18c1d578 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -49,9 +49,7 @@ class ClassProvider implements ServiceProviderInterface 'ProjectNotification', 'ProjectMetadata', 'ProjectGroupRole', - 'ProjectGroupRoleFilter', 'ProjectUserRole', - 'ProjectUserRoleFilter', 'RememberMeSession', 'Subtask', 'SubtaskTimeTracking', @@ -63,7 +61,6 @@ class ClassProvider implements ServiceProviderInterface 'TaskExternalLink', 'TaskFinder', 'TaskFile', - 'TaskFilter', 'TaskLink', 'TaskModification', 'TaskPermission', @@ -79,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/FilterProvider.php b/app/ServiceProvider/FilterProvider.php new file mode 100644 index 00000000..555cb262 --- /dev/null +++ b/app/ServiceProvider/FilterProvider.php @@ -0,0 +1,112 @@ +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['userQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(User::TABLE)); + return $builder; + }); + + $container['projectQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(Project::TABLE)); + return $builder; + }); + + $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 TaskCreationDateFilter()) + ->withFilter(new TaskDescriptionFilter()) + ->withFilter(new TaskDueDateFilter()) + ->withFilter(new TaskIdFilter()) + ->withFilter(TaskLinkFilter::getInstance() + ->setDatabase($c['db']) + ) + ->withFilter(new TaskModificationDateFilter()) + ->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 43a78e32..3590afa5 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'); diff --git a/app/common.php b/app/common.php index 7dbd7587..da624844 100644 --- a/app/common.php +++ b/app/common.php @@ -39,4 +39,5 @@ $container->register(new Kanboard\ServiceProvider\RouteProvider); $container->register(new Kanboard\ServiceProvider\ActionProvider); $container->register(new Kanboard\ServiceProvider\ExternalLinkProvider); $container->register(new Kanboard\ServiceProvider\AvatarProvider); +$container->register(new Kanboard\ServiceProvider\FilterProvider); $container->register(new Kanboard\ServiceProvider\PluginProvider); diff --git a/composer.lock b/composer.lock index 438118a2..70881a39 100644 --- a/composer.lock +++ b/composer.lock @@ -9,16 +9,16 @@ "packages": [ { "name": "christian-riesen/base32", - "version": "1.2.2", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/ChristianRiesen/base32.git", - "reference": "fbe67d49d45dc789f942ef828c787550ebb894bc" + "reference": "fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/fbe67d49d45dc789f942ef828c787550ebb894bc", - "reference": "fbe67d49d45dc789f942ef828c787550ebb894bc", + "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4", + "reference": "fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4", "shasum": "" }, "require": { @@ -59,7 +59,7 @@ "encode", "rfc4648" ], - "time": "2015-09-27 23:45:02" + "time": "2016-04-07 07:45:31" }, { "name": "christian-riesen/otp", @@ -397,16 +397,16 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.1", + "version": "v2.0.2", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "76e90f747b769b347fe584e8015a014549107d35" + "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/76e90f747b769b347fe584e8015a014549107d35", - "reference": "76e90f747b769b347fe584e8015a014549107d35", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/088c04e2f261c33bed6ca5245491cfca69195ccf", + "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf", "shasum": "" }, "require": { @@ -441,7 +441,7 @@ "pseudorandom", "random" ], - "time": "2016-03-18 20:36:13" + "time": "2016-04-03 06:00:07" }, { "name": "pimple/pimple", diff --git a/doc/installation.markdown b/doc/installation.markdown index dd4283f8..c796ac65 100644 --- a/doc/installation.markdown +++ b/doc/installation.markdown @@ -29,7 +29,7 @@ From the repository (development version) You must install [composer](https://getcomposer.org/) to use this method. 1. `git clone https://github.com/fguillot/kanboard.git` -2. `composer install` +2. `composer install --no-dev` 3. Go to the third step just above Note: This method will install the **current development version**, use at your own risk. diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown index 5dc56cd1..a00aba16 100644 --- a/doc/plugin-hooks.markdown +++ b/doc/plugin-hooks.markdown @@ -28,15 +28,6 @@ Some hooks can have only one listener: - `$start` (DateTime) - `$end` (DateTime) -#### model:subtask-time-tracking:calendar:events - -- Override subtask time tracking events to display the calendar -- Arguments: - - `$user_id` (integer) - - `$events` (array) - - `$start` (string, ISO-8601 format) - - `$end` (string, ISO-8601 format) - ### Merge hooks "Merge hooks" act in the same way as the function `array_merge`. The hook callback must return an array. This array will be merged with the default one. diff --git a/doc/update.markdown b/doc/update.markdown index 7be8a65a..12ac152d 100644 --- a/doc/update.markdown +++ b/doc/update.markdown @@ -27,7 +27,7 @@ From the repository (development version) ----------------------------------------- 1. `git pull` -2. `composer install` +2. `composer install --no-dev` 3. Login and check if everything is ok Note: This method will install the **current development version**, use at your own risk. diff --git a/tests/units/Base.php b/tests/units/Base.php index 563035f6..5125ffb9 100644 --- a/tests/units/Base.php +++ b/tests/units/Base.php @@ -40,6 +40,7 @@ abstract class Base extends PHPUnit_Framework_TestCase $this->container->register(new Kanboard\ServiceProvider\NotificationProvider); $this->container->register(new Kanboard\ServiceProvider\RouteProvider); $this->container->register(new Kanboard\ServiceProvider\AvatarProvider); + $this->container->register(new Kanboard\ServiceProvider\FilterProvider); $this->container['dispatcher'] = new TraceableEventDispatcher( new EventDispatcher, diff --git a/tests/units/Core/Filter/LexerBuilderTest.php b/tests/units/Core/Filter/LexerBuilderTest.php new file mode 100644 index 00000000..ac5315bb --- /dev/null +++ b/tests/units/Core/Filter/LexerBuilderTest.php @@ -0,0 +1,106 @@ +container); + $taskCreation = new TaskCreation($this->container); + $taskFinder = new TaskFinder($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $project->create(array('name' => 'Project'))); + $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'Test'))); + + $builder = new LexerBuilder(); + $builder->withFilter(new TaskAssigneeFilter()); + $builder->withFilter(new TaskTitleFilter(), true); + $builder->withQuery($query); + $tasks = $builder->build('assignee:nobody')->toArray(); + + $this->assertCount(1, $tasks); + $this->assertEquals('Test', $tasks[0]['title']); + } + + public function testBuilderThatReturnNothing() + { + $project = new Project($this->container); + $taskCreation = new TaskCreation($this->container); + $taskFinder = new TaskFinder($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $project->create(array('name' => 'Project'))); + $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'Test'))); + + $builder = new LexerBuilder(); + $builder->withFilter(new TaskAssigneeFilter()); + $builder->withFilter(new TaskTitleFilter(), true); + $builder->withQuery($query); + $tasks = $builder->build('something')->toArray(); + + $this->assertCount(0, $tasks); + } + + public function testBuilderWithEmptyInput() + { + $project = new Project($this->container); + $taskCreation = new TaskCreation($this->container); + $taskFinder = new TaskFinder($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $project->create(array('name' => 'Project'))); + $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'Test'))); + + $builder = new LexerBuilder(); + $builder->withFilter(new TaskAssigneeFilter()); + $builder->withFilter(new TaskTitleFilter(), true); + $builder->withQuery($query); + $tasks = $builder->build('')->toArray(); + + $this->assertCount(1, $tasks); + } + + public function testBuilderWithMultipleMatches() + { + $project = new Project($this->container); + $taskCreation = new TaskCreation($this->container); + $taskFinder = new TaskFinder($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $project->create(array('name' => 'Project'))); + $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'ABC', 'owner_id' => 1))); + $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'DEF'))); + + $builder = new LexerBuilder(); + $builder->withFilter(new TaskAssigneeFilter()); + $builder->withFilter(new TaskTitleFilter(), true); + $builder->withQuery($query); + $tasks = $builder->build('assignee:nobody assignee:1')->toArray(); + + $this->assertCount(2, $tasks); + } + + public function testClone() + { + $taskFinder = new TaskFinder($this->container); + $query = $taskFinder->getExtendedQuery(); + + $builder = new LexerBuilder(); + $builder->withFilter(new TaskAssigneeFilter()); + $builder->withFilter(new TaskTitleFilter()); + $builder->withQuery($query); + + $clone = clone($builder); + $this->assertFalse($builder === $clone); + $this->assertFalse($builder->build('test')->getQuery() === $clone->build('test')->getQuery()); + } +} diff --git a/tests/units/Core/Filter/LexerTest.php b/tests/units/Core/Filter/LexerTest.php new file mode 100644 index 00000000..3f3e368e --- /dev/null +++ b/tests/units/Core/Filter/LexerTest.php @@ -0,0 +1,100 @@ +assertSame(array(), $lexer->tokenize('This is Kanboard')); + } + + public function testTokenizeWithDefaultToken() + { + $lexer = new Lexer(); + $lexer->setDefaultToken('myDefaultToken'); + + $expected = array( + 'myDefaultToken' => array('This is Kanboard'), + ); + + $this->assertSame($expected, $lexer->tokenize('This is Kanboard')); + } + + public function testTokenizeWithCustomToken() + { + $lexer = new Lexer(); + $lexer->addToken("/^(assignee:)/", 'T_USER'); + + $expected = array( + 'T_USER' => array('admin'), + ); + + $this->assertSame($expected, $lexer->tokenize('assignee:admin something else')); + } + + public function testTokenizeWithCustomTokenAndDefaultToken() + { + $lexer = new Lexer(); + $lexer->setDefaultToken('myDefaultToken'); + $lexer->addToken("/^(assignee:)/", 'T_USER'); + + $expected = array( + 'T_USER' => array('admin'), + 'myDefaultToken' => array('something else'), + ); + + $this->assertSame($expected, $lexer->tokenize('assignee:admin something else')); + } + + public function testTokenizeWithQuotedString() + { + $lexer = new Lexer(); + $lexer->addToken("/^(assignee:)/", 'T_USER'); + + $expected = array( + 'T_USER' => array('Foo Bar'), + ); + + $this->assertSame($expected, $lexer->tokenize('assignee:"Foo Bar" something else')); + } + + public function testTokenizeWithNumber() + { + $lexer = new Lexer(); + $lexer->setDefaultToken('myDefaultToken'); + + $expected = array( + 'myDefaultToken' => array('#123'), + ); + + $this->assertSame($expected, $lexer->tokenize('#123')); + } + + public function testTokenizeWithStringDate() + { + $lexer = new Lexer(); + $lexer->addToken("/^(date:)/", 'T_DATE'); + + $expected = array( + 'T_DATE' => array('today'), + ); + + $this->assertSame($expected, $lexer->tokenize('date:today something else')); + } + + public function testTokenizeWithIsoDate() + { + $lexer = new Lexer(); + $lexer->addToken("/^(date:)/", 'T_DATE'); + + $expected = array( + 'T_DATE' => array('<=2016-01-01'), + ); + + $this->assertSame($expected, $lexer->tokenize('date:<=2016-01-01 something else')); + } +} diff --git a/tests/units/Core/Filter/OrCriteriaTest.php b/tests/units/Core/Filter/OrCriteriaTest.php new file mode 100644 index 00000000..787d3461 --- /dev/null +++ b/tests/units/Core/Filter/OrCriteriaTest.php @@ -0,0 +1,58 @@ +container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $userModel = new User($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 2))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1))); + + $criteria = new OrCriteria(); + $criteria->withQuery($query); + $criteria->withFilter(TaskAssigneeFilter::getInstance(1)); + $criteria->withFilter(TaskAssigneeFilter::getInstance(2)); + $criteria->apply(); + + $this->assertCount(2, $query->findAll()); + } + + public function testWithDifferentFilter() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $userModel = new User($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'ABC', 'project_id' => 1, 'owner_id' => 2))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'DEF', 'project_id' => 1, 'owner_id' => 1))); + + $criteria = new OrCriteria(); + $criteria->withQuery($query); + $criteria->withFilter(TaskAssigneeFilter::getInstance(1)); + $criteria->withFilter(TaskTitleFilter::getInstance('ABC')); + $criteria->apply(); + + $this->assertCount(2, $query->findAll()); + } +} diff --git a/tests/units/Core/LexerTest.php b/tests/units/Core/LexerTest.php deleted file mode 100644 index 55370aab..00000000 --- a/tests/units/Core/LexerTest.php +++ /dev/null @@ -1,468 +0,0 @@ -assertEquals( - array(array('match' => 'swimlane:', 'token' => 'T_SWIMLANE'), array('match' => 'Version 42', 'token' => 'T_STRING')), - $lexer->tokenize('swimlane:"Version 42"') - ); - - $this->assertEquals( - array(array('match' => 'swimlane:', 'token' => 'T_SWIMLANE'), array('match' => 'v3', 'token' => 'T_STRING')), - $lexer->tokenize('swimlane:v3') - ); - - $this->assertEquals( - array('T_SWIMLANE' => array('v3')), - $lexer->map($lexer->tokenize('swimlane:v3')) - ); - - $this->assertEquals( - array('T_SWIMLANE' => array('Version 42', 'v3')), - $lexer->map($lexer->tokenize('swimlane:"Version 42" swimlane:v3')) - ); - } - - public function testAssigneeQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'me', 'token' => 'T_STRING')), - $lexer->tokenize('assignee:me') - ); - - $this->assertEquals( - array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'everybody', 'token' => 'T_STRING')), - $lexer->tokenize('assignee:everybody') - ); - - $this->assertEquals( - array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'nobody', 'token' => 'T_STRING')), - $lexer->tokenize('assignee:nobody') - ); - - $this->assertEquals( - array('T_ASSIGNEE' => array('nobody')), - $lexer->map($lexer->tokenize('assignee:nobody')) - ); - - $this->assertEquals( - array('T_ASSIGNEE' => array('John Doe', 'me')), - $lexer->map($lexer->tokenize('assignee:"John Doe" assignee:me')) - ); - } - - public function testColorQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'color:', 'token' => 'T_COLOR'), array('match' => 'Blue', 'token' => 'T_STRING')), - $lexer->tokenize('color:Blue') - ); - - $this->assertEquals( - array(array('match' => 'color:', 'token' => 'T_COLOR'), array('match' => 'Dark Grey', 'token' => 'T_STRING')), - $lexer->tokenize('color:"Dark Grey"') - ); - - $this->assertEquals( - array('T_COLOR' => array('Blue')), - $lexer->map($lexer->tokenize('color:Blue')) - ); - - $this->assertEquals( - array('T_COLOR' => array('Dark Grey')), - $lexer->map($lexer->tokenize('color:"Dark Grey"')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('color: ')) - ); - } - - public function testCategoryQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'category:', 'token' => 'T_CATEGORY'), array('match' => 'Feature Request', 'token' => 'T_STRING')), - $lexer->tokenize('category:"Feature Request"') - ); - - $this->assertEquals( - array('T_CATEGORY' => array('Feature Request')), - $lexer->map($lexer->tokenize('category:"Feature Request"')) - ); - - $this->assertEquals( - array('T_CATEGORY' => array('Feature Request', 'Bug')), - $lexer->map($lexer->tokenize('category:"Feature Request" category:Bug')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('category: ')) - ); - } - - public function testLinkQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'link:', 'token' => 'T_LINK'), array('match' => 'is a milestone of', 'token' => 'T_STRING')), - $lexer->tokenize('link:"is a milestone of"') - ); - - $this->assertEquals( - array('T_LINK' => array('is a milestone of')), - $lexer->map($lexer->tokenize('link:"is a milestone of"')) - ); - - $this->assertEquals( - array('T_LINK' => array('is a milestone of', 'fixes')), - $lexer->map($lexer->tokenize('link:"is a milestone of" link:fixes')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('link: ')) - ); - } - - public function testColumnQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'column:', 'token' => 'T_COLUMN'), array('match' => 'Feature Request', 'token' => 'T_STRING')), - $lexer->tokenize('column:"Feature Request"') - ); - - $this->assertEquals( - array('T_COLUMN' => array('Feature Request')), - $lexer->map($lexer->tokenize('column:"Feature Request"')) - ); - - $this->assertEquals( - array('T_COLUMN' => array('Feature Request', 'Bug')), - $lexer->map($lexer->tokenize('column:"Feature Request" column:Bug')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('column: ')) - ); - } - - public function testProjectQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'project:', 'token' => 'T_PROJECT'), array('match' => 'My project', 'token' => 'T_STRING')), - $lexer->tokenize('project:"My project"') - ); - - $this->assertEquals( - array('T_PROJECT' => array('My project')), - $lexer->map($lexer->tokenize('project:"My project"')) - ); - - $this->assertEquals( - array('T_PROJECT' => array('My project', 'plop')), - $lexer->map($lexer->tokenize('project:"My project" project:plop')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('project: ')) - ); - } - - public function testStatusQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'status:', 'token' => 'T_STATUS'), array('match' => 'open', 'token' => 'T_STRING')), - $lexer->tokenize('status:open') - ); - - $this->assertEquals( - array(array('match' => 'status:', 'token' => 'T_STATUS'), array('match' => 'closed', 'token' => 'T_STRING')), - $lexer->tokenize('status:closed') - ); - - $this->assertEquals( - array('T_STATUS' => 'open'), - $lexer->map($lexer->tokenize('status:open')) - ); - - $this->assertEquals( - array('T_STATUS' => 'closed'), - $lexer->map($lexer->tokenize('status:closed')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('status: ')) - ); - } - - public function testReferenceQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'ref:', 'token' => 'T_REFERENCE'), array('match' => '123', 'token' => 'T_STRING')), - $lexer->tokenize('ref:123') - ); - - $this->assertEquals( - array(array('match' => 'reference:', 'token' => 'T_REFERENCE'), array('match' => '456', 'token' => 'T_STRING')), - $lexer->tokenize('reference:456') - ); - - $this->assertEquals( - array('T_REFERENCE' => '123'), - $lexer->map($lexer->tokenize('reference:123')) - ); - - $this->assertEquals( - array('T_REFERENCE' => '456'), - $lexer->map($lexer->tokenize('ref:456')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('ref: ')) - ); - } - - public function testDescriptionQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'description:', 'token' => 'T_DESCRIPTION'), array('match' => 'my text search', 'token' => 'T_STRING')), - $lexer->tokenize('description:"my text search"') - ); - - $this->assertEquals( - array('T_DESCRIPTION' => 'my text search'), - $lexer->map($lexer->tokenize('description:"my text search"')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('description: ')) - ); - } - - public function testDueDateQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('due:2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '<2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('due:<2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '>2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('due:>2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '<=2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('due:<=2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '>=2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('due:>=2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => 'yesterday', 'token' => 'T_DATE')), - $lexer->tokenize('due:yesterday') - ); - - $this->assertEquals( - array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => 'tomorrow', 'token' => 'T_DATE')), - $lexer->tokenize('due:tomorrow') - ); - - $this->assertEquals( - array(), - $lexer->tokenize('due:#2015-05-01') - ); - - $this->assertEquals( - array(), - $lexer->tokenize('due:01-05-1024') - ); - - $this->assertEquals( - array('T_DUE' => '2015-05-01'), - $lexer->map($lexer->tokenize('due:2015-05-01')) - ); - - $this->assertEquals( - array('T_DUE' => '<2015-05-01'), - $lexer->map($lexer->tokenize('due:<2015-05-01')) - ); - - $this->assertEquals( - array('T_DUE' => 'today'), - $lexer->map($lexer->tokenize('due:today')) - ); - } - - public function testModifiedQuery() - { - $lexer = new Lexer; - - $this->assertEquals( - array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('modified:2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '<2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('modified:<2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '>2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('modified:>2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => '<=2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('updated:<=2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => '>=2015-05-01', 'token' => 'T_DATE')), - $lexer->tokenize('updated:>=2015-05-01') - ); - - $this->assertEquals( - array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => 'yesterday', 'token' => 'T_DATE')), - $lexer->tokenize('updated:yesterday') - ); - - $this->assertEquals( - array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => 'tomorrow', 'token' => 'T_DATE')), - $lexer->tokenize('updated:tomorrow') - ); - - $this->assertEquals( - array(), - $lexer->tokenize('updated:#2015-05-01') - ); - - $this->assertEquals( - array(), - $lexer->tokenize('modified:01-05-1024') - ); - - $this->assertEquals( - array('T_UPDATED' => '2015-05-01'), - $lexer->map($lexer->tokenize('modified:2015-05-01')) - ); - - $this->assertEquals( - array('T_UPDATED' => '<2015-05-01'), - $lexer->map($lexer->tokenize('modified:<2015-05-01')) - ); - - $this->assertEquals( - array('T_UPDATED' => 'today'), - $lexer->map($lexer->tokenize('modified:today')) - ); - } - - public function testMultipleCriterias() - { - $lexer = new Lexer; - - $this->assertEquals( - array('T_COLOR' => array('Dark Grey'), 'T_ASSIGNEE' => array('Fred G'), 'T_TITLE' => 'my task title'), - $lexer->map($lexer->tokenize('color:"Dark Grey" assignee:"Fred G" my task title')) - ); - - $this->assertEquals( - array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow')), - $lexer->map($lexer->tokenize('my title color:yellow')) - ); - - $this->assertEquals( - array('T_TITLE' => 'my title', 'T_DUE' => '2015-04-01'), - $lexer->map($lexer->tokenize('my title due:2015-04-01')) - ); - - $this->assertEquals( - array('T_TITLE' => 'awesome', 'T_DUE' => '<=2015-04-01'), - $lexer->map($lexer->tokenize('due:<=2015-04-01 awesome')) - ); - - $this->assertEquals( - array('T_TITLE' => 'awesome', 'T_DUE' => 'today'), - $lexer->map($lexer->tokenize('due:today awesome')) - ); - - $this->assertEquals( - array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow'), 'T_DUE' => '2015-04-01'), - $lexer->map($lexer->tokenize('my title color:yellow due:2015-04-01')) - ); - - $this->assertEquals( - array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow'), 'T_DUE' => '2015-04-01', 'T_ASSIGNEE' => array('John Doe')), - $lexer->map($lexer->tokenize('my title color:yellow due:2015-04-01 assignee:"John Doe"')) - ); - - $this->assertEquals( - array('T_TITLE' => 'my title'), - $lexer->map($lexer->tokenize('my title color:')) - ); - - $this->assertEquals( - array('T_TITLE' => 'my title'), - $lexer->map($lexer->tokenize('my title color:assignee:')) - ); - - $this->assertEquals( - array('T_TITLE' => 'my title'), - $lexer->map($lexer->tokenize('my title ')) - ); - - $this->assertEquals( - array('T_TITLE' => '#123'), - $lexer->map($lexer->tokenize('#123')) - ); - - $this->assertEquals( - array(), - $lexer->map($lexer->tokenize('color:assignee:')) - ); - } -} diff --git a/tests/units/Filter/TaskAssigneeFilterTest.php b/tests/units/Filter/TaskAssigneeFilterTest.php new file mode 100644 index 00000000..356342c5 --- /dev/null +++ b/tests/units/Filter/TaskAssigneeFilterTest.php @@ -0,0 +1,159 @@ +container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1))); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue(1); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue(123); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } + + public function testWithStringAssigneeId() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1))); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue('1'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue("123"); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } + + public function testWithUsername() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1))); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue('admin'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue('foobar'); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } + + public function testWithName() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $userModel = new User($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 2))); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue('foo bar'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue('bob'); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } + + public function testWithNobody() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + + $filter = new TaskAssigneeFilter(); + $filter->withQuery($query); + $filter->withValue('nobody'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + } + + public function testWithCurrentUser() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1))); + + $filter = new TaskAssigneeFilter(); + $filter->setCurrentUserId(1); + $filter->withQuery($query); + $filter->withValue('me'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskAssigneeFilter(); + $filter->setCurrentUserId(2); + $filter->withQuery($query); + $filter->withValue('me'); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } +} diff --git a/tests/units/Formatter/TaskFilterCalendarFormatterTest.php b/tests/units/Formatter/TaskFilterCalendarFormatterTest.php deleted file mode 100644 index 09dd0de6..00000000 --- a/tests/units/Formatter/TaskFilterCalendarFormatterTest.php +++ /dev/null @@ -1,21 +0,0 @@ -container); - $filter1 = $tf->create()->setFullDay(); - $filter2 = $tf->copy(); - - $this->assertTrue($filter1 !== $filter2); - $this->assertTrue($filter1->query !== $filter2->query); - $this->assertTrue($filter1->query->condition !== $filter2->query->condition); - $this->assertTrue($filter1->isFullDay()); - $this->assertFalse($filter2->isFullDay()); - } -} diff --git a/tests/units/Formatter/TaskFilterGanttFormatterTest.php b/tests/units/Formatter/TaskFilterGanttFormatterTest.php deleted file mode 100644 index 14804784..00000000 --- a/tests/units/Formatter/TaskFilterGanttFormatterTest.php +++ /dev/null @@ -1,24 +0,0 @@ -container); - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilterGanttFormatter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1'))); - - $this->assertNotEmpty($tf->search('status:open')->format()); - } -} diff --git a/tests/units/Formatter/TaskFilterICalendarFormatterTest.php b/tests/units/Formatter/TaskFilterICalendarFormatterTest.php deleted file mode 100644 index 6de9cf0f..00000000 --- a/tests/units/Formatter/TaskFilterICalendarFormatterTest.php +++ /dev/null @@ -1,74 +0,0 @@ -container); - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilterICalendarFormatter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'creator_id' => 1, 'date_due' => $dp->getTimestampFromIsoFormat('-2 days')))); - - $ics = $tf->create() - ->filterByDueDateRange(strtotime('-1 month'), strtotime('+1 month')) - ->setFullDay() - ->setCalendar(new Calendar('Kanboard')) - ->setColumns('date_due') - ->addFullDayEvents() - ->format(); - - $this->assertContains('UID:task-#1-date_due', $ics); - $this->assertContains('DTSTART;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('-2 days')), $ics); - $this->assertContains('DTEND;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('-2 days')), $ics); - $this->assertContains('URL:http://localhost/?controller=task&action=show&task_id=1&project_id=1', $ics); - $this->assertContains('SUMMARY:#1 task1', $ics); - $this->assertContains('ATTENDEE:MAILTO:admin@kanboard.local', $ics); - $this->assertContains('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE', $ics); - } - - public function testIcalEventsWithAssigneeAndDueDate() - { - $dp = new DateParser($this->container); - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilterICalendarFormatter($this->container); - $u = new User($this->container); - $c = new Config($this->container); - - $this->assertNotFalse($c->save(array('application_url' => 'http://kb/'))); - $this->assertEquals('http://kb/', $c->get('application_url')); - - $this->assertNotFalse($u->update(array('id' => 1, 'email' => 'bob@localhost'))); - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'owner_id' => 1, 'date_due' => $dp->getTimestampFromIsoFormat('+5 days')))); - - $ics = $tf->create() - ->filterByDueDateRange(strtotime('-1 month'), strtotime('+1 month')) - ->setFullDay() - ->setCalendar(new Calendar('Kanboard')) - ->setColumns('date_due') - ->addFullDayEvents() - ->format(); - - $this->assertContains('UID:task-#1-date_due', $ics); - $this->assertContains('DTSTART;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('+5 days')), $ics); - $this->assertContains('DTEND;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('+5 days')), $ics); - $this->assertContains('URL:http://kb/?controller=task&action=show&task_id=1&project_id=1', $ics); - $this->assertContains('SUMMARY:#1 task1', $ics); - $this->assertContains('ORGANIZER;CN=admin:MAILTO:bob@localhost', $ics); - $this->assertContains('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE', $ics); - } -} diff --git a/tests/units/Model/SubtaskTimeTrackingTest.php b/tests/units/Model/SubtaskTimeTrackingTest.php index 9fa8d5b0..2545dcb2 100644 --- a/tests/units/Model/SubtaskTimeTrackingTest.php +++ b/tests/units/Model/SubtaskTimeTrackingTest.php @@ -240,81 +240,4 @@ class SubtaskTimeTrackingTest extends Base $this->assertEquals(0, $task['time_estimated']); $this->assertEquals(0, $task['time_spent']); } - - public function testGetCalendarEvents() - { - $tf = new TaskFinder($this->container); - $tc = new TaskCreation($this->container); - $s = new Subtask($this->container); - $st = new SubtaskTimeTracking($this->container); - $p = new Project($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test1'))); - $this->assertEquals(2, $p->create(array('name' => 'test2'))); - - $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'test 1', 'project_id' => 2))); - - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1))); - $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 1))); - - $this->assertEquals(4, $s->create(array('title' => 'subtask #4', 'task_id' => 2))); - $this->assertEquals(5, $s->create(array('title' => 'subtask #5', 'task_id' => 2))); - $this->assertEquals(6, $s->create(array('title' => 'subtask #6', 'task_id' => 2))); - $this->assertEquals(7, $s->create(array('title' => 'subtask #7', 'task_id' => 2))); - $this->assertEquals(8, $s->create(array('title' => 'subtask #8', 'task_id' => 2))); - - // Slot start before and finish inside the calendar time range - $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 1, 'start' => strtotime('-1 day'), 'end' => strtotime('+1 hour'))); - - // Slot start inside time range and finish after the time range - $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 2, 'start' => strtotime('+1 hour'), 'end' => strtotime('+2 days'))); - - // Start before time range and finish inside time range - $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 3, 'start' => strtotime('-1 day'), 'end' => strtotime('+1.5 days'))); - - // Start and finish inside time range - $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 4, 'start' => strtotime('+1 hour'), 'end' => strtotime('+2 hours'))); - - // Start and finish after the time range - $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 5, 'start' => strtotime('+2 days'), 'end' => strtotime('+3 days'))); - - // Start and finish before the time range - $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 6, 'start' => strtotime('-2 days'), 'end' => strtotime('-1 day'))); - - // Start before time range and not finished - $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 7, 'start' => strtotime('-1 day'))); - - // Start inside time range and not finish - $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 8, 'start' => strtotime('+3200 seconds'))); - - $timesheet = $st->getUserTimesheet(1); - $this->assertNotEmpty($timesheet); - $this->assertCount(8, $timesheet); - - $events = $st->getUserCalendarEvents(1, date('Y-m-d'), date('Y-m-d', strtotime('+2 day'))); - $this->assertNotEmpty($events); - $this->assertCount(6, $events); - $this->assertEquals(1, $events[0]['subtask_id']); - $this->assertEquals(2, $events[1]['subtask_id']); - $this->assertEquals(3, $events[2]['subtask_id']); - $this->assertEquals(4, $events[3]['subtask_id']); - $this->assertEquals(7, $events[4]['subtask_id']); - $this->assertEquals(8, $events[5]['subtask_id']); - - $events = $st->getProjectCalendarEvents(1, date('Y-m-d'), date('Y-m-d', strtotime('+2 days'))); - $this->assertNotEmpty($events); - $this->assertCount(3, $events); - $this->assertEquals(1, $events[0]['subtask_id']); - $this->assertEquals(2, $events[1]['subtask_id']); - $this->assertEquals(3, $events[2]['subtask_id']); - - $events = $st->getProjectCalendarEvents(2, date('Y-m-d'), date('Y-m-d', strtotime('+2 days'))); - $this->assertNotEmpty($events); - $this->assertCount(3, $events); - $this->assertEquals(4, $events[0]['subtask_id']); - $this->assertEquals(7, $events[1]['subtask_id']); - $this->assertEquals(8, $events[2]['subtask_id']); - } } diff --git a/tests/units/Model/TaskFilterTest.php b/tests/units/Model/TaskFilterTest.php deleted file mode 100644 index 9e291c31..00000000 --- a/tests/units/Model/TaskFilterTest.php +++ /dev/null @@ -1,624 +0,0 @@ -container); - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time())))); - - $this->assertEmpty($tf->search('search something')->findAll()); - } - - public function testSearchWithEmptyInput() - { - $dp = new DateParser($this->container); - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time())))); - - $result = $tf->search('')->findAll(); - $this->assertNotEmpty($result); - $this->assertCount(4, $result); - } - - public function testSearchById() - { - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task 43'))); - - $tf->search('#2'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - - $tf->search('1'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - - $tf->search('something'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('#'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('#abcd'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('task1'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - - $tf->search('43'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task 43', $tasks[0]['title']); - } - - public function testSearchWithReference() - { - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'reference' => 123))); - - $tf->search('ref:123'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - - $tf->search('reference:123'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - - $tf->search('ref:plop'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('ref:'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - } - - public function testSearchWithStatus() - { - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'is_active' => 0))); - - $tf->search('status:open'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - - $tf->search('status:plop'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(3, $tasks); - - $tf->search('status:closed'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - } - - public function testSearchWithDescription() - { - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'description' => '**something to do**'))); - - $tf->search('description:"something"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - - $tf->search('description:"rainy day"'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - } - - public function testSearchWithCategory() - { - $p = new Project($this->container); - $c = new Category($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(1, $c->create(array('name' => 'Feature request', 'project_id' => 1))); - $this->assertEquals(2, $c->create(array('name' => 'hé hé', 'project_id' => 1))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'category_id' => 1))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task3', 'category_id' => 2))); - - $tf->search('category:"Feature request"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - $this->assertEquals('Feature request', $tasks[0]['category_name']); - - $tf->search('category:"hé hé"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task3', $tasks[0]['title']); - $this->assertEquals('hé hé', $tasks[0]['category_name']); - - $tf->search('category:"Feature request" category:"hé hé"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - $this->assertEquals('Feature request', $tasks[0]['category_name']); - $this->assertEquals('task3', $tasks[1]['title']); - $this->assertEquals('hé hé', $tasks[1]['category_name']); - - $tf->search('category:none'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('', $tasks[0]['category_name']); - - $tf->search('category:"not found"'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - } - - public function testSearchWithProject() - { - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'My project A'))); - $this->assertEquals(2, $p->create(array('name' => 'My project B'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1'))); - $this->assertNotFalse($tc->create(array('project_id' => 2, 'title' => 'task2'))); - - $tf->search('project:"My project A"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('My project A', $tasks[0]['project_name']); - - $tf->search('project:2'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - $this->assertEquals('My project B', $tasks[0]['project_name']); - - $tf->search('project:"My project A" project:"my project b"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('My project A', $tasks[0]['project_name']); - $this->assertEquals('task2', $tasks[1]['title']); - $this->assertEquals('My project B', $tasks[1]['project_name']); - - $tf->search('project:"not found"'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - } - - public function testSearchWithSwimlane() - { - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - $s = new Swimlane($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'My project A'))); - $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'Version 1.1'))); - $this->assertEquals(2, $s->create(array('project_id' => 1, 'name' => 'Version 1.2'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'swimlane_id' => 1))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'swimlane_id' => 2))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task3', 'swimlane_id' => 0))); - - $tf->search('swimlane:"Version 1.1"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('Version 1.1', $tasks[0]['swimlane_name']); - - $tf->search('swimlane:"versioN 1.2"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - $this->assertEquals('Version 1.2', $tasks[0]['swimlane_name']); - - $tf->search('swimlane:"Default swimlane"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task3', $tasks[0]['title']); - $this->assertEquals('Default swimlane', $tasks[0]['default_swimlane']); - $this->assertEquals('', $tasks[0]['swimlane_name']); - - $tf->search('swimlane:default'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task3', $tasks[0]['title']); - $this->assertEquals('Default swimlane', $tasks[0]['default_swimlane']); - $this->assertEquals('', $tasks[0]['swimlane_name']); - - $tf->search('swimlane:"Version 1.1" swimlane:"Version 1.2"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('Version 1.1', $tasks[0]['swimlane_name']); - $this->assertEquals('task2', $tasks[1]['title']); - $this->assertEquals('Version 1.2', $tasks[1]['swimlane_name']); - - $tf->search('swimlane:"not found"'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - } - - public function testSearchWithColumn() - { - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'My project A'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'column_id' => 3))); - - $tf->search('column:Backlog'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('Backlog', $tasks[0]['column_name']); - - $tf->search('column:backlog column:"Work in progress"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('Backlog', $tasks[0]['column_name']); - $this->assertEquals('task2', $tasks[1]['title']); - $this->assertEquals('Work in progress', $tasks[1]['column_name']); - - $tf->search('column:"not found"'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - } - - public function testSearchWithDueDate() - { - $dp = new DateParser($this->container); - $p = new Project($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day')))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time())))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'no due date'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'due date at 0', 'date_due' => 0))); - - $tf->search('due:>'.date('Y-m-d')); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('my task title is amazing', $tasks[0]['title']); - - $tf->search('due:>='.date('Y-m-d')); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('my task title is amazing', $tasks[0]['title']); - $this->assertEquals('youpi', $tasks[1]['title']); - - $tf->search('due:<'.date('Y-m-d')); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('my task title is awesome', $tasks[0]['title']); - $this->assertEquals('Bob at work', $tasks[1]['title']); - - $tf->search('due:<='.date('Y-m-d')); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(3, $tasks); - $this->assertEquals('my task title is awesome', $tasks[0]['title']); - $this->assertEquals('Bob at work', $tasks[1]['title']); - $this->assertEquals('youpi', $tasks[2]['title']); - - $tf->search('due:tomorrow'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('my task title is amazing', $tasks[0]['title']); - - $tf->search('due:yesterday'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('Bob at work', $tasks[0]['title']); - - $tf->search('due:today'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('youpi', $tasks[0]['title']); - } - - public function testSearchWithColor() - { - $p = new Project($this->container); - $u = new User($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'color_id' => 'light_green'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'color_id' => 'blue'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work'))); - - $tf->search('color:"Light Green"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('my task title is awesome', $tasks[0]['title']); - - $tf->search('color:"Light Green" amazing'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('color:"plop'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('color:unknown'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(3, $tasks); - - $tf->search('color:blue amazing'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('my task title is amazing', $tasks[0]['title']); - - $tf->search('color:blue color:Yellow'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('my task title is amazing', $tasks[0]['title']); - $this->assertEquals('Bob at work', $tasks[1]['title']); - } - - public function testSearchWithAssignee() - { - $p = new Project($this->container); - $u = new User($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'owner_id' => 1))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'owner_id' => 0))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'owner_id' => 2))); - - $tf->search('assignee:john'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('assignee:admin my task title'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('my task title is awesome', $tasks[0]['title']); - - $tf->search('my task title'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('my task title is awesome', $tasks[0]['title']); - $this->assertEquals('my task title is amazing', $tasks[1]['title']); - - $tf->search('my task title assignee:nobody'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('my task title is amazing', $tasks[0]['title']); - - $tf->search('assignee:"Bob ryan" assignee:nobody'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('my task title is amazing', $tasks[0]['title']); - $this->assertEquals('Bob at work', $tasks[1]['title']); - } - - public function testSearchWithAssigneeIncludingSubtasks() - { - $p = new Project($this->container); - $u = new User($this->container); - $tc = new TaskCreation($this->container); - $s = new Subtask($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Paul Ryan'))); - - $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'task1', 'owner_id' => 2))); - $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'status' => 1, 'user_id' => 0))); - - $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'task2', 'owner_id' => 0))); - $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 2, 'status' => 1, 'user_id' => 2))); - - $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'task3', 'owner_id' => 0))); - $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 3, 'user_id' => 1))); - - $tf->search('assignee:bob'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('task2', $tasks[1]['title']); - - $tf->search('assignee:"Paul Ryan"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('task1', $tasks[0]['title']); - $this->assertEquals('task2', $tasks[1]['title']); - - $tf->search('assignee:nobody'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(2, $tasks); - $this->assertEquals('task2', $tasks[0]['title']); - $this->assertEquals('task3', $tasks[1]['title']); - - $tf->search('assignee:admin'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('task3', $tasks[0]['title']); - } - - public function testSearchWithLink() - { - $p = new Project($this->container); - $u = new User($this->container); - $tc = new TaskCreation($this->container); - $tl = new TaskLink($this->container); - $tf = new TaskFilter($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'test'))); - $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'color_id' => 'light_green'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'color_id' => 'blue'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work'))); - $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'I have a bad feeling about that'))); - $this->assertEquals(1, $tl->create(1, 2, 9)); // #1 is a milestone of #2 - $this->assertEquals(3, $tl->create(2, 1, 2)); // #2 blocks #1 - $this->assertEquals(5, $tl->create(3, 2, 2)); // #3 blocks #2 - - $tf->search('link:"is a milestone of"'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('my task title is awesome', $tasks[0]['title']); - - $tf->search('link:"is a milestone of" amazing'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('link:"unknown"'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('link:unknown'); - $tasks = $tf->findAll(); - $this->assertEmpty($tasks); - - $tf->search('link:blocks amazing'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(1, $tasks); - $this->assertEquals('my task title is amazing', $tasks[0]['title']); - - $tf->search('link:"is a milestone of" link:blocks'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(3, $tasks); - $this->assertEquals('my task title is awesome', $tasks[0]['title']); - $this->assertEquals('my task title is amazing', $tasks[1]['title']); - $this->assertEquals('Bob at work', $tasks[2]['title']); - - $tf->search('link:"is a milestone of" link:blocks link:unknown'); - $tasks = $tf->findAll(); - $this->assertNotEmpty($tasks); - $this->assertCount(3, $tasks); - $this->assertEquals('my task title is awesome', $tasks[0]['title']); - $this->assertEquals('my task title is amazing', $tasks[1]['title']); - $this->assertEquals('Bob at work', $tasks[2]['title']); - } - - public function testCopy() - { - $tf = new TaskFilter($this->container); - $filter1 = $tf->create(); - $filter2 = $tf->copy(); - - $this->assertTrue($filter1 !== $filter2); - $this->assertTrue($filter1->query !== $filter2->query); - $this->assertTrue($filter1->query->condition !== $filter2->query->condition); - } -} -- cgit v1.2.3 From 7705f4c533c3db726624e639be72fc9822904e96 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 9 Apr 2016 23:24:26 -0400 Subject: Added search in comments --- ChangeLog | 4 +++ app/Filter/TaskCommentFilter.php | 41 ++++++++++++++++++++++ app/ServiceProvider/FilterProvider.php | 2 ++ doc/search.markdown | 23 ++++++++---- tests/units/Filter/TaskCommentFilterTest.php | 52 ++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 app/Filter/TaskCommentFilter.php create mode 100644 tests/units/Filter/TaskCommentFilterTest.php (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index ea12d9b9..941c46c9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,10 @@ Version 1.0.28 (unreleased) -------------- +New features: + +* Search in comments + Improvements: * Filter/Lexer/QueryBuilder refactoring 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 @@ +query->ilike(Comment::TABLE.'.comment', '%'.$this->value.'%'); + $this->query->join(Comment::TABLE, 'task_id', 'id', Task::TABLE); + + return $this; + } +} diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php index 555cb262..66608b8c 100644 --- a/app/ServiceProvider/FilterProvider.php +++ b/app/ServiceProvider/FilterProvider.php @@ -8,6 +8,7 @@ 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\TaskDescriptionFilter; use Kanboard\Filter\TaskDueDateFilter; @@ -85,6 +86,7 @@ class FilterProvider implements ServiceProviderInterface ->withFilter(new TaskCategoryFilter()) ->withFilter(TaskColorFilter::getInstance()->setColorModel($c['color'])) ->withFilter(new TaskColumnFilter()) + ->withFilter(new TaskCommentFilter()) ->withFilter(new TaskCreationDateFilter()) ->withFilter(new TaskDescriptionFilter()) ->withFilter(new TaskDueDateFilter()) diff --git a/doc/search.markdown b/doc/search.markdown index 1a97a7fc..93c8214e 100644 --- a/doc/search.markdown +++ b/doc/search.markdown @@ -38,7 +38,12 @@ Attribute: **assignee** - Query for unassigned tasks: `assignee:nobody` - Query for my assigned tasks: `assignee:me` -Note: Kanboard will also search in assigned subtasks with the status todo and in progress. +Search by subtask assignee +-------------------------- + +Attribute: **subtask:assignee** + +- Example: `subtask:assignee:"John Doe"` Search by color --------------- @@ -90,7 +95,7 @@ Works in the same way as the modification date queries. Search by description --------------------- -Attribute: **description** +Attribute: **description** or **desc** Example: `description:"text search"` @@ -127,14 +132,14 @@ Attribute: **column** - Find tasks by column name: `column:"Work in progress"` - Find tasks for several columns: `column:"Backlog" column:ready` -Search by swim lane +Search by swim-lane ------------------- Attribute: **swimlane** -- Find tasks by swim lane: `swimlane:"Version 42"` -- Find tasks in the default swim lane: `swimlane:default` -- Find tasks into several swim lanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"` +- Find tasks by swim-lane: `swimlane:"Version 42"` +- Find tasks in the default swim-lane: `swimlane:default` +- Find tasks into several swim-lanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"` Search by task link ------------------ @@ -144,3 +149,9 @@ Attribute: **link** - Find tasks by link name: `link:"is a milestone of"` - Find tasks into several links: `link:"is a milestone of" link:"relates to"` +Search by comment +----------------- + +Attribute: **comment** + +- Find comments that contains this title: `comment:"My comment message"` diff --git a/tests/units/Filter/TaskCommentFilterTest.php b/tests/units/Filter/TaskCommentFilterTest.php new file mode 100644 index 00000000..8d1b7f44 --- /dev/null +++ b/tests/units/Filter/TaskCommentFilterTest.php @@ -0,0 +1,52 @@ +container); + $taskCreation = new TaskCreation($this->container); + $commentModel = new Comment($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'user_id' => 1, 'comment' => 'This is a test'))); + + $filter = new TaskCommentFilter(); + $filter->withQuery($query); + $filter->withValue('test'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + } + + public function testNoMatch() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $commentModel = new Comment($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(1, $commentModel->create(array('task_id' => 1, 'user_id' => 1, 'comment' => 'This is a test'))); + + $filter = new TaskCommentFilter(); + $filter->withQuery($query); + $filter->withValue('foobar'); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } +} -- cgit v1.2.3 From 38326c4ddf91ed54374775d7f7599136f3e38eea Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 10 Apr 2016 08:15:10 -0400 Subject: Added search by task creator --- ChangeLog | 1 + app/Filter/TaskCreatorFilter.php | 74 +++++++++++++ app/Model/TaskFinder.php | 1 + app/ServiceProvider/FilterProvider.php | 8 +- doc/search.markdown | 13 ++- tests/units/Filter/TaskCreatorFilterTest.php | 159 +++++++++++++++++++++++++++ 6 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 app/Filter/TaskCreatorFilter.php create mode 100644 tests/units/Filter/TaskCreatorFilterTest.php (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 941c46c9..1bbe8062 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,7 @@ Version 1.0.28 (unreleased) New features: * Search in comments +* Search by task creator Improvements: 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 @@ +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/Model/TaskFinder.php b/app/Model/TaskFinder.php index 1840b505..d406b794 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -138,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) diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php index 66608b8c..3100ae7e 100644 --- a/app/ServiceProvider/FilterProvider.php +++ b/app/ServiceProvider/FilterProvider.php @@ -10,6 +10,7 @@ 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; @@ -84,10 +85,15 @@ class FilterProvider implements ServiceProviderInterface ->setCurrentUserId($c['userSession']->getId()) ) ->withFilter(new TaskCategoryFilter()) - ->withFilter(TaskColorFilter::getInstance()->setColorModel($c['color'])) + ->withFilter(TaskColorFilter::getInstance() + ->setColorModel($c['color']) + ) ->withFilter(new TaskColumnFilter()) ->withFilter(new TaskCommentFilter()) ->withFilter(new TaskCreationDateFilter()) + ->withFilter(TaskCreatorFilter::getInstance() + ->setCurrentUserId($c['userSession']->getId()) + ) ->withFilter(new TaskDescriptionFilter()) ->withFilter(new TaskDueDateFilter()) ->withFilter(new TaskIdFilter()) diff --git a/doc/search.markdown b/doc/search.markdown index 93c8214e..f6d343e9 100644 --- a/doc/search.markdown +++ b/doc/search.markdown @@ -27,8 +27,8 @@ Attribute: **status** - Query to find open tasks: `status:open` - Query to find closed tasks: `status:closed` -Search by assignees -------------------- +Search by assignee +------------------ Attribute: **assignee** @@ -38,6 +38,15 @@ Attribute: **assignee** - Query for unassigned tasks: `assignee:nobody` - Query for my assigned tasks: `assignee:me` +Search by task creator +---------------------- + +Attribute: **creator** + +- Tasks created by myself: `creator:me` +- Tasks created by John Doe: `creator:"John Doe"` +- Tasks created by the user id #1: `creator:1` + Search by subtask assignee -------------------------- diff --git a/tests/units/Filter/TaskCreatorFilterTest.php b/tests/units/Filter/TaskCreatorFilterTest.php new file mode 100644 index 00000000..1c344de7 --- /dev/null +++ b/tests/units/Filter/TaskCreatorFilterTest.php @@ -0,0 +1,159 @@ +container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 1))); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue(1); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue(123); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } + + public function testWithStringAssigneeId() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 1))); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue('1'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue("123"); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } + + public function testWithUsername() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 1))); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue('admin'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue('foobar'); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } + + public function testWithName() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $userModel = new User($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar'))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 2))); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue('foo bar'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue('bob'); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } + + public function testWithNobody() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + + $filter = new TaskCreatorFilter(); + $filter->withQuery($query); + $filter->withValue('nobody'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + } + + public function testWithCurrentUser() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $query = $taskFinder->getExtendedQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'Test'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'creator_id' => 1))); + + $filter = new TaskCreatorFilter(); + $filter->setCurrentUserId(1); + $filter->withQuery($query); + $filter->withValue('me'); + $filter->apply(); + + $this->assertCount(1, $query->findAll()); + + $filter = new TaskCreatorFilter(); + $filter->setCurrentUserId(2); + $filter->withQuery($query); + $filter->withValue('me'); + $filter->apply(); + + $this->assertCount(0, $query->findAll()); + } +} -- cgit v1.2.3 From 9f0166502b8b8886156bcb4ad0497cd9ee5a60b2 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 10 Apr 2016 15:18:20 -0400 Subject: Added search in activity stream --- ChangeLog | 1 + app/Controller/Search.php | 18 ++++ app/Filter/BaseDateFilter.php | 103 ++++++++++++++++++ app/Filter/BaseFilter.php | 44 -------- app/Filter/ProjectActivityCreationDateFilter.php | 38 +++++++ app/Filter/ProjectActivityCreatorFilter.php | 65 ++++++++++++ app/Filter/ProjectActivityProjectIdsFilter.php | 2 +- app/Filter/ProjectActivityProjectNameFilter.php | 38 +++++++ app/Filter/ProjectActivityTaskStatusFilter.php | 43 ++++++++ app/Filter/ProjectActivityTaskTitleFilter.php | 15 +-- app/Filter/TaskCompletionDateFilter.php | 2 +- app/Filter/TaskCreationDateFilter.php | 2 +- app/Filter/TaskDueDateFilter.php | 2 +- app/Filter/TaskModificationDateFilter.php | 2 +- app/Filter/TaskProjectsFilter.php | 7 +- app/Filter/TaskStartDateFilter.php | 2 +- app/Helper/ProjectActivityHelper.php | 27 +++++ app/Locale/bs_BA/translations.php | 10 ++ app/Locale/cs_CZ/translations.php | 10 ++ app/Locale/da_DK/translations.php | 10 ++ app/Locale/de_DE/translations.php | 10 ++ app/Locale/el_GR/translations.php | 10 ++ app/Locale/es_ES/translations.php | 10 ++ app/Locale/fi_FI/translations.php | 10 ++ app/Locale/fr_FR/translations.php | 10 ++ app/Locale/hu_HU/translations.php | 10 ++ app/Locale/id_ID/translations.php | 10 ++ app/Locale/it_IT/translations.php | 10 ++ app/Locale/ja_JP/translations.php | 10 ++ app/Locale/ko_KR/translations.php | 10 ++ app/Locale/my_MY/translations.php | 10 ++ app/Locale/nb_NO/translations.php | 10 ++ app/Locale/nl_NL/translations.php | 10 ++ app/Locale/pl_PL/translations.php | 10 ++ app/Locale/pt_BR/translations.php | 10 ++ app/Locale/pt_PT/translations.php | 10 ++ app/Locale/ru_RU/translations.php | 12 ++- app/Locale/sr_Latn_RS/translations.php | 10 ++ app/Locale/sv_SE/translations.php | 10 ++ app/Locale/th_TH/translations.php | 10 ++ app/Locale/tr_TR/translations.php | 10 ++ app/Locale/zh_CN/translations.php | 10 ++ app/Model/ProjectActivity.php | 1 + app/ServiceProvider/FilterProvider.php | 30 +++++- app/ServiceProvider/RouteProvider.php | 3 +- app/Template/activity/filter_dropdown.php | 14 +++ app/Template/search/activity.php | 39 +++++++ app/Template/search/index.php | 4 +- doc/search.markdown | 83 +++++++++------ .../ProjectActivityCreationDateFilterTest.php | 117 +++++++++++++++++++++ .../Filter/ProjectActivityCreatorFilterTest.php | 91 ++++++++++++++++ .../ProjectActivityProjectNameFilterTest.php | 35 ++++++ .../Filter/ProjectActivityTaskStatusFilterTest.php | 49 +++++++++ .../Filter/ProjectActivityTaskTitleFilterTest.php | 47 ++++++++- 54 files changed, 1066 insertions(+), 110 deletions(-) create mode 100644 app/Filter/BaseDateFilter.php create mode 100644 app/Filter/ProjectActivityCreationDateFilter.php create mode 100644 app/Filter/ProjectActivityCreatorFilter.php create mode 100644 app/Filter/ProjectActivityProjectNameFilter.php create mode 100644 app/Filter/ProjectActivityTaskStatusFilter.php create mode 100644 app/Template/activity/filter_dropdown.php create mode 100644 app/Template/search/activity.php create mode 100644 tests/units/Filter/ProjectActivityCreationDateFilterTest.php create mode 100644 tests/units/Filter/ProjectActivityCreatorFilterTest.php create mode 100644 tests/units/Filter/ProjectActivityProjectNameFilterTest.php create mode 100644 tests/units/Filter/ProjectActivityTaskStatusFilterTest.php (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 1bbe8062..f4952b53 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ Version 1.0.28 (unreleased) New features: +* Search in activity stream * Search in comments * Search by task creator diff --git a/app/Controller/Search.php b/app/Controller/Search.php index 840a90c8..a42e9d3d 100644 --- a/app/Controller/Search.php +++ b/app/Controller/Search.php @@ -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/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 @@ +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 index a7e6a61a..79a664be 100644 --- a/app/Filter/BaseFilter.php +++ b/app/Filter/BaseFilter.php @@ -72,48 +72,4 @@ abstract class BaseFilter $this->value = $value; 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) - { - $timestamp = strtotime($this->value); - $method = $this->parseOperator(); - - if ($method !== '') { - $this->query->$method($field, $timestamp); - } else { - $this->query->gte($field, $timestamp); - $this->query->lte($field, $timestamp + 86399); - } - } } 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 @@ +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 @@ +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/ProjectActivityProjectIdsFilter.php b/app/Filter/ProjectActivityProjectIdsFilter.php index 4d7c9028..47cf0c25 100644 --- a/app/Filter/ProjectActivityProjectIdsFilter.php +++ b/app/Filter/ProjectActivityProjectIdsFilter.php @@ -21,7 +21,7 @@ class ProjectActivityProjectIdsFilter extends BaseFilter implements FilterInterf */ public function getAttributes() { - return array('project_ids'); + return array('projects'); } /** 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 @@ +query->ilike(Project::TABLE.'.name', '%'.$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 @@ +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 index ed3f36d6..bf2afa30 100644 --- a/app/Filter/ProjectActivityTaskTitleFilter.php +++ b/app/Filter/ProjectActivityTaskTitleFilter.php @@ -3,7 +3,6 @@ namespace Kanboard\Filter; use Kanboard\Core\Filter\FilterInterface; -use Kanboard\Model\Task; /** * Filter activity events by task title @@ -11,7 +10,7 @@ use Kanboard\Model\Task; * @package filter * @author Frederic Guillot */ -class ProjectActivityTaskTitleFilter extends BaseFilter implements FilterInterface +class ProjectActivityTaskTitleFilter extends TaskTitleFilter implements FilterInterface { /** * Get search attribute @@ -23,16 +22,4 @@ class ProjectActivityTaskTitleFilter extends BaseFilter implements FilterInterfa { return array('title'); } - - /** - * Apply filter - * - * @access public - * @return FilterInterface - */ - public function apply() - { - $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%'); - return $this; - } } diff --git a/app/Filter/TaskCompletionDateFilter.php b/app/Filter/TaskCompletionDateFilter.php index 5166bebf..f206a3e2 100644 --- a/app/Filter/TaskCompletionDateFilter.php +++ b/app/Filter/TaskCompletionDateFilter.php @@ -11,7 +11,7 @@ use Kanboard\Model\Task; * @package filter * @author Frederic Guillot */ -class TaskCompletionDateFilter extends BaseFilter implements FilterInterface +class TaskCompletionDateFilter extends BaseDateFilter implements FilterInterface { /** * Get search attribute diff --git a/app/Filter/TaskCreationDateFilter.php b/app/Filter/TaskCreationDateFilter.php index 26318b3e..bb6efad6 100644 --- a/app/Filter/TaskCreationDateFilter.php +++ b/app/Filter/TaskCreationDateFilter.php @@ -11,7 +11,7 @@ use Kanboard\Model\Task; * @package filter * @author Frederic Guillot */ -class TaskCreationDateFilter extends BaseFilter implements FilterInterface +class TaskCreationDateFilter extends BaseDateFilter implements FilterInterface { /** * Get search attribute diff --git a/app/Filter/TaskDueDateFilter.php b/app/Filter/TaskDueDateFilter.php index 6ba55eb9..e36efdd0 100644 --- a/app/Filter/TaskDueDateFilter.php +++ b/app/Filter/TaskDueDateFilter.php @@ -11,7 +11,7 @@ use Kanboard\Model\Task; * @package filter * @author Frederic Guillot */ -class TaskDueDateFilter extends BaseFilter implements FilterInterface +class TaskDueDateFilter extends BaseDateFilter implements FilterInterface { /** * Get search attribute diff --git a/app/Filter/TaskModificationDateFilter.php b/app/Filter/TaskModificationDateFilter.php index d8838bce..5036e9c1 100644 --- a/app/Filter/TaskModificationDateFilter.php +++ b/app/Filter/TaskModificationDateFilter.php @@ -11,7 +11,7 @@ use Kanboard\Model\Task; * @package filter * @author Frederic Guillot */ -class TaskModificationDateFilter extends BaseFilter implements FilterInterface +class TaskModificationDateFilter extends BaseDateFilter implements FilterInterface { /** * Get search attribute diff --git a/app/Filter/TaskProjectsFilter.php b/app/Filter/TaskProjectsFilter.php index e0fc09cf..47636b1d 100644 --- a/app/Filter/TaskProjectsFilter.php +++ b/app/Filter/TaskProjectsFilter.php @@ -32,7 +32,12 @@ class TaskProjectsFilter extends BaseFilter implements FilterInterface */ public function apply() { - $this->query->in(Task::TABLE.'.project_id', $this->value); + 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/TaskStartDateFilter.php b/app/Filter/TaskStartDateFilter.php index d45bc0d4..dd30762b 100644 --- a/app/Filter/TaskStartDateFilter.php +++ b/app/Filter/TaskStartDateFilter.php @@ -11,7 +11,7 @@ use Kanboard\Model\Task; * @package filter * @author Frederic Guillot */ -class TaskStartDateFilter extends BaseFilter implements FilterInterface +class TaskStartDateFilter extends BaseDateFilter implements FilterInterface { /** * Get search attribute diff --git a/app/Helper/ProjectActivityHelper.php b/app/Helper/ProjectActivityHelper.php index 738fec66..0638a978 100644 --- a/app/Helper/ProjectActivityHelper.php +++ b/app/Helper/ProjectActivityHelper.php @@ -17,6 +17,33 @@ use Kanboard\Model\ProjectActivity; */ 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 * diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index 7ca864f4..e384f923 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index b2921de9..3c8de1ad 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index c4743922..747fa2d1 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 999bf048..fa447e62 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1153,4 +1153,14 @@ return array( '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.' => '', + // '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' => '', ); diff --git a/app/Locale/el_GR/translations.php b/app/Locale/el_GR/translations.php index 9a31e485..84cf8462 100644 --- a/app/Locale/el_GR/translations.php +++ b/app/Locale/el_GR/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index c3623369..e52c959b 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 8e5dd81f..f47852b0 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index cedd6039..0c2e4955 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -1153,4 +1153,14 @@ return array( 'Upload my avatar image' => 'Uploader mon image d\'avatar', 'Remove my image' => 'Supprimer mon image', 'The OAuth2 state parameter is invalid' => 'Le paramètre "state" de OAuth2 est invalide', + 'User not found.' => 'Utilisateur introuvable.', + 'Search in activity stream' => 'Chercher dans le flux d\'activité', + 'My activities' => 'Mes activités', + 'Activity until yesterday' => 'Activités jusqu\'à hier', + 'Activity until today' => 'Activités jusqu\'à aujourd\'hui', + 'Search by creator: ' => 'Rechercher par créateur : ', + 'Search by creation date: ' => 'Rechercher par date de création : ', + 'Search by task status: ' => 'Rechercher par le statut des tâches : ', + 'Search by task title: ' => 'Rechercher par le titre des tâches : ', + 'Activity stream search' => 'Recherche dans le flux d\'activité', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index f642a6c1..9a2d666a 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index 3f105054..9cbca60e 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 93ceb03f..d0209b3a 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index b48eabd8..69ab5f17 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/ko_KR/translations.php b/app/Locale/ko_KR/translations.php index 8379761f..f4320c55 100644 --- a/app/Locale/ko_KR/translations.php +++ b/app/Locale/ko_KR/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/my_MY/translations.php b/app/Locale/my_MY/translations.php index 36b3db0b..f6f15937 100644 --- a/app/Locale/my_MY/translations.php +++ b/app/Locale/my_MY/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 465efb53..f3d3047a 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 3c3fa1ee..f08f5eff 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index d06e347f..8222f9e1 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index cdb06dea..60242d95 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1153,4 +1153,14 @@ return array( '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' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index e38344f8..956d1259 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -1153,4 +1153,14 @@ return array( '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.' => '', + // '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' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index b3503e52..1e548e0d 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -1152,5 +1152,15 @@ return array( 'Avatar' => 'Аватар', 'Upload my avatar image' => 'Загрузить моё изображение для аватара', 'Remove my image' => 'Удалить моё изображение', - 'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный' + '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' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index c7070a8d..b69e6cf4 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index e4728d2d..634b87d0 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 1e2fb98a..1e913f28 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index 6e8fae2f..95bcc8a8 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index decd49d8..7b0c3139 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -1153,4 +1153,14 @@ return array( // 'Upload my avatar image' => '', // 'Remove my image' => '', // 'The OAuth2 state parameter is invalid' => '', + // 'User not found.' => '', + // 'Search in activity stream' => '', + // 'My activities' => '', + // 'Activity until yesterday' => '', + // 'Activity until today' => '', + // 'Search by creator: ' => '', + // 'Search by creation date: ' => '', + // 'Search by task status: ' => '', + // 'Search by task title: ' => '', + // 'Activity stream search' => '', ); diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 31cee113..d993015b 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -71,6 +71,7 @@ class ProjectActivity extends Base 'uc.avatar_path' ) ->join(Task::TABLE, 'id', 'task_id') + ->join(Project::TABLE, 'id', 'project_id') ->left(User::TABLE, 'uc', 'id', ProjectActivity::TABLE, 'creator_id'); } diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php index 4b4dbd2d..f3918d77 100644 --- a/app/ServiceProvider/FilterProvider.php +++ b/app/ServiceProvider/FilterProvider.php @@ -4,6 +4,10 @@ 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; @@ -86,8 +90,18 @@ class FilterProvider implements ServiceProviderInterface $container['projectActivityLexer'] = $container->factory(function ($c) { $builder = new LexerBuilder(); - $builder->withQuery($c['projectActivity']->getQuery()); - $builder->withFilter(new ProjectActivityTaskTitleFilter()); + $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; }); @@ -124,17 +138,23 @@ class FilterProvider implements ServiceProviderInterface ) ->withFilter(new TaskColumnFilter()) ->withFilter(new TaskCommentFilter()) - ->withFilter(new TaskCreationDateFilter()) + ->withFilter(TaskCreationDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) ->withFilter(TaskCreatorFilter::getInstance() ->setCurrentUserId($c['userSession']->getId()) ) ->withFilter(new TaskDescriptionFilter()) - ->withFilter(new TaskDueDateFilter()) + ->withFilter(TaskDueDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) ->withFilter(new TaskIdFilter()) ->withFilter(TaskLinkFilter::getInstance() ->setDatabase($c['db']) ) - ->withFilter(new TaskModificationDateFilter()) + ->withFilter(TaskModificationDateFilter::getInstance() + ->setDateParser($c['dateParser']) + ) ->withFilter(new TaskProjectFilter()) ->withFilter(new TaskReferenceFilter()) ->withFilter(new TaskStatusFilter()) 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 @@ + \ No newline at end of file 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 @@ +
+ + +
+ + form->hidden('controller', $values) ?> + form->hidden('action', $values) ?> + form->text('search', $values, array(), array(empty($values['search']) ? 'autofocus' : '', 'placeholder="'.t('Search').'"'), 'form-input-large') ?> + render('activity/filter_dropdown') ?> + +
+ + +
+

+

project:"My project" creator:me

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

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

+
+ +

+ + render('event/events', array('events' => $events)) ?> + + +
\ 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 @@ diff --git a/doc/search.markdown b/doc/search.markdown index f6d343e9..37bb8625 100644 --- a/doc/search.markdown +++ b/doc/search.markdown @@ -1,7 +1,8 @@ Advanced Search Syntax ====================== -Kanboard uses a simple query language for advanced search. +Kanboard uses a simple query language for advanced search. +You can search in tasks, comments, subtasks, links but also in the activity stream. Example of query ---------------- @@ -12,23 +13,23 @@ This example will return all tasks assigned to me with a due date for tomorrow a assigne:me due:tomorrow my title ``` -Search by task id or title --------------------------- +Global search +------------- + +### Search by task id or title - Search by task id: `#123` - Search by task id and task title: `123` - Search by task title: anything that doesn't match any search attributes -Search by status ----------------- +### Search by status Attribute: **status** - Query to find open tasks: `status:open` - Query to find closed tasks: `status:closed` -Search by assignee ------------------- +### Search by assignee Attribute: **assignee** @@ -38,8 +39,7 @@ Attribute: **assignee** - Query for unassigned tasks: `assignee:nobody` - Query for my assigned tasks: `assignee:me` -Search by task creator ----------------------- +### Search by task creator Attribute: **creator** @@ -47,23 +47,20 @@ Attribute: **creator** - Tasks created by John Doe: `creator:"John Doe"` - Tasks created by the user id #1: `creator:1` -Search by subtask assignee --------------------------- +### Search by subtask assignee Attribute: **subtask:assignee** - Example: `subtask:assignee:"John Doe"` -Search by color ---------------- +### Search by color Attribute: **color** - Query to search by color id: `color:blue` - Query to search by color name: `color:"Deep Orange"` -Search by the due date ----------------------- +### Search by the due date Attribute: **due** @@ -83,8 +80,7 @@ Operators supported with a date: - Greater than or equal: **due:>=2015-06-29** - Lower than or equal: **due:<=2015-06-29** -Search by modification date ---------------------------- +### Search by modification date Attribute: **modified** or **updated** @@ -94,29 +90,25 @@ There is also a filter by recently modified tasks: `modified:recently`. This query will use the same value as the board highlight period configured in settings. -Search by creation date ------------------------ +### Search by creation date Attribute: **created** Works in the same way as the modification date queries. -Search by description ---------------------- +### Search by description Attribute: **description** or **desc** Example: `description:"text search"` -Search by external reference ----------------------------- +### Search by external reference The task reference is an external id of your task, by example a ticket number from another software. - Find tasks with a reference: `ref:1234` or `reference:TICKET-1234` -Search by category ------------------- +### Search by category Attribute: **category** @@ -124,8 +116,7 @@ Attribute: **category** - Find all tasks that have those categories: `category:"Bug" category:"Improvements"` - Find tasks with no category assigned: `category:none` -Search by project ------------------ +### Search by project Attribute: **project** @@ -133,16 +124,14 @@ Attribute: **project** - Find tasks by project id: `project:23` - Find tasks for several projects: `project:"My project A" project:"My project B"` -Search by columns ------------------ +### Search by columns Attribute: **column** - Find tasks by column name: `column:"Work in progress"` - Find tasks for several columns: `column:"Backlog" column:ready` -Search by swim-lane -------------------- +### Search by swim-lane Attribute: **swimlane** @@ -150,17 +139,41 @@ Attribute: **swimlane** - Find tasks in the default swim-lane: `swimlane:default` - Find tasks into several swim-lanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"` -Search by task link ------------------- +### Search by task link Attribute: **link** - Find tasks by link name: `link:"is a milestone of"` - Find tasks into several links: `link:"is a milestone of" link:"relates to"` -Search by comment ------------------ +### Search by comment Attribute: **comment** - Find comments that contains this title: `comment:"My comment message"` + +Activity stream search +---------------------- + +### Search events by task title + +Attribute: **title** or none (default) + +- Example: `title:"My task"` +- Search by task id: `#123` + +### Search events by task status + +Attribute: **status** + +### Search by event creator + +Attribute: **creator** + +### Search by event creation date + +Attribute: **created** + +### Search events by project + +Attribute: **project** diff --git a/tests/units/Filter/ProjectActivityCreationDateFilterTest.php b/tests/units/Filter/ProjectActivityCreationDateFilterTest.php new file mode 100644 index 00000000..d679f285 --- /dev/null +++ b/tests/units/Filter/ProjectActivityCreationDateFilterTest.php @@ -0,0 +1,117 @@ +container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreationDateFilter('today'); + $filter->setDateParser($this->container['dateParser']); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + } + + public function testWithYesterday() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreationDateFilter('yesterday'); + $filter->setDateParser($this->container['dateParser']); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(0, $events); + } + + public function testWithIsoDate() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreationDateFilter(date('Y-m-d')); + $filter->setDateParser($this->container['dateParser']); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + } + + public function testWithOperatorAndIsoDate() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreationDateFilter('>='.date('Y-m-d')); + $filter->setDateParser($this->container['dateParser']); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreationDateFilter('<'.date('Y-m-d')); + $filter->setDateParser($this->container['dateParser']); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(0, $events); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreationDateFilter('>'.date('Y-m-d')); + $filter->setDateParser($this->container['dateParser']); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(0, $events); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreationDateFilter('>='.date('Y-m-d')); + $filter->setDateParser($this->container['dateParser']); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + } +} diff --git a/tests/units/Filter/ProjectActivityCreatorFilterTest.php b/tests/units/Filter/ProjectActivityCreatorFilterTest.php new file mode 100644 index 00000000..99c70322 --- /dev/null +++ b/tests/units/Filter/ProjectActivityCreatorFilterTest.php @@ -0,0 +1,91 @@ +container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreatorFilter('admin'); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + } + + public function testWithAnotherUsername() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreatorFilter('John Doe'); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(0, $events); + } + + public function testWithCurrentUser() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreatorFilter('me'); + $filter->setCurrentUserId(1); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + } + + public function testWithAnotherCurrentUser() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityCreatorFilter('me'); + $filter->setCurrentUserId(2); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(0, $events); + } +} diff --git a/tests/units/Filter/ProjectActivityProjectNameFilterTest.php b/tests/units/Filter/ProjectActivityProjectNameFilterTest.php new file mode 100644 index 00000000..de9d7d59 --- /dev/null +++ b/tests/units/Filter/ProjectActivityProjectNameFilterTest.php @@ -0,0 +1,35 @@ +container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + $query = $projectActivityModel->getQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + $this->assertEquals(2, $projectModel->create(array('name' => 'P2'))); + + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2))); + + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + $this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2)))); + + $filter = new ProjectActivityProjectNameFilter('P1'); + $filter->withQuery($query)->apply(); + $this->assertCount(1, $query->findAll()); + } +} diff --git a/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php b/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php new file mode 100644 index 00000000..b8df6338 --- /dev/null +++ b/tests/units/Filter/ProjectActivityTaskStatusFilterTest.php @@ -0,0 +1,49 @@ +container); + $taskCreation = new TaskCreation($this->container); + $taskStatus = new TaskStatus($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2)))); + + $this->assertTrue($taskStatus->close(1)); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityTaskStatusFilter('open'); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + $this->assertEquals(2, $events[0]['task_id']); + + $query = $projectActivityModel->getQuery(); + $filter = new ProjectActivityTaskStatusFilter('closed'); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + $this->assertEquals(1, $events[0]['task_id']); + } +} diff --git a/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php b/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php index 6a7c23af..925a1ab2 100644 --- a/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php +++ b/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php @@ -11,7 +11,7 @@ require_once __DIR__.'/../Base.php'; class ProjectActivityTaskTitleFilterTest extends Base { - public function testFilterByTaskId() + public function testWithFullTitle() { $taskFinder = new TaskFinder($this->container); $taskCreation = new TaskCreation($this->container); @@ -31,4 +31,49 @@ class ProjectActivityTaskTitleFilterTest extends Base $filter->withQuery($query)->apply(); $this->assertCount(1, $query->findAll()); } + + public function testWithPartialTitle() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + $query = $projectActivityModel->getQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1))); + + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2)))); + + $filter = new ProjectActivityTaskTitleFilter('test'); + $filter->withQuery($query)->apply(); + $this->assertCount(2, $query->findAll()); + } + + public function testWithId() + { + $taskFinder = new TaskFinder($this->container); + $taskCreation = new TaskCreation($this->container); + $projectModel = new Project($this->container); + $projectActivityModel = new ProjectActivity($this->container); + $query = $projectActivityModel->getQuery(); + + $this->assertEquals(1, $projectModel->create(array('name' => 'P1'))); + + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1))); + + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1)))); + $this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2)))); + + $filter = new ProjectActivityTaskTitleFilter('#2'); + $filter->withQuery($query)->apply(); + + $events = $query->findAll(); + $this->assertCount(1, $events); + $this->assertEquals(2, $events[0]['task_id']); + } } -- cgit v1.2.3 From 06b86313c9b33899dcec1a430f1c3016e193c1d9 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Mon, 11 Apr 2016 21:06:20 -0400 Subject: Removed PHP notices in comment suppression view --- ChangeLog | 4 ++++ app/Model/Comment.php | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index f4952b53..6282ecee 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,10 @@ Improvements: * Filter/Lexer/QueryBuilder refactoring +Bug fixes: + +* Removed PHP notices in comment suppression view + Version 1.0.27 -------------- diff --git a/app/Model/Comment.php b/app/Model/Comment.php index f7ac4eaa..c5091d89 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -76,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) -- cgit v1.2.3 From 63387fa9cfdb8bd20356fce9729a5a49a9f78bb9 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Tue, 12 Apr 2016 22:26:44 -0400 Subject: Added command line utility to reset user password and to disable 2FA --- ChangeLog | 1 + app/Console/BaseCommand.php | 2 + app/Console/ResetPasswordCommand.php | 79 +++++++++++++++++++++++++++++++++++ app/Console/ResetTwoFactorCommand.php | 38 +++++++++++++++++ doc/cli.markdown | 17 ++++++++ kanboard | 4 ++ 6 files changed, 141 insertions(+) create mode 100644 app/Console/ResetPasswordCommand.php create mode 100644 app/Console/ResetTwoFactorCommand.php (limited to 'ChangeLog') diff --git a/ChangeLog b/ChangeLog index 6282ecee..398ec499 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,7 @@ New features: * Search in activity stream * Search in comments * Search by task creator +* Added command line utility to reset user password and to disable 2FA Improvements: diff --git a/app/Console/BaseCommand.php b/app/Console/BaseCommand.php index bf86ae0d..23cdcc9c 100644 --- a/app/Console/BaseCommand.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,6 +22,7 @@ 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 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 @@ +setName('user:reset-password') + ->setDescription('Change user password') + ->addArgument('username', InputArgument::REQUIRED, 'Username') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $helper = $this->getHelper('question'); + $username = $input->getArgument('username'); + + $passwordQuestion = new Question('What is the new password for '.$username.'? (characters are not printed)'.PHP_EOL); + $passwordQuestion->setHidden(true); + $passwordQuestion->setHiddenFallback(false); + + $password = $helper->ask($input, $output, $passwordQuestion); + + $confirmationQuestion = new Question('Confirmation:'.PHP_EOL); + $confirmationQuestion->setHidden(true); + $confirmationQuestion->setHiddenFallback(false); + + $confirmation = $helper->ask($input, $output, $confirmationQuestion); + + if ($this->validatePassword($output, $password, $confirmation)) { + $this->resetPassword($output, $username, $password); + } + } + + private function validatePassword(OutputInterface $output, $password, $confirmation) + { + list($valid, $errors) = $this->passwordResetValidator->validateModification(array( + 'password' => $password, + 'confirmation' => $confirmation, + )); + + if (!$valid) { + foreach ($errors as $error_list) { + foreach ($error_list as $error) { + $output->writeln(''.$error.''); + } + } + } + + return $valid; + } + + private function resetPassword(OutputInterface $output, $username, $password) + { + $userId = $this->user->getIdByUsername($username); + + if (empty($userId)) { + $output->writeln('User not found'); + return false; + } + + if (!$this->user->update(array('id' => $userId, 'password' => $password))) { + $output->writeln('Unable to update password'); + return false; + } + + $output->writeln('Password updated successfully'); + + 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 @@ +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('User not found'); + return false; + } + + if (!$this->user->update(array('id' => $userId, 'twofactor_activated' => 0, 'twofactor_secret' => ''))) { + $output->writeln('Unable to update user profile'); + return false; + } + + $output->writeln('Two-factor authentication disabled'); + + return true; + } +} diff --git a/doc/cli.markdown b/doc/cli.markdown index 9334d84b..d38d8b53 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -45,6 +45,9 @@ Available commands: projects:daily-stats Calculate daily statistics for all projects trigger trigger:tasks Trigger scheduler event for all tasks + user + user:reset-2fa Remove two-factor authentication for a user + user:reset-password Change user password ``` Available commands @@ -147,3 +150,17 @@ This command send a "daily cronjob event" to all open tasks of each project. ./kanboard trigger:tasks Trigger task event: project_id=2, nb_tasks=1 ``` + +### Reset user password + +```bash +./kanboard user:reset-password my_user +``` + +You will be prompted for a password and confirmation. Characters are not printed to the screen. + +### Remove two-factor authentication for a user + +```bash +./kanboard user:reset-2fa my_user +``` diff --git a/kanboard b/kanboard index 8ac49d79..6a51c937 100755 --- a/kanboard +++ b/kanboard @@ -3,6 +3,8 @@ require __DIR__.'/app/common.php'; +use Kanboard\Console\ResetPasswordCommand; +use Kanboard\Console\ResetTwoFactorCommand; use Symfony\Component\Console\Application; use Symfony\Component\EventDispatcher\Event; use Kanboard\Console\TaskOverdueNotificationCommand; @@ -29,4 +31,6 @@ $application->add(new LocaleSyncCommand($container)); $application->add(new LocaleComparatorCommand($container)); $application->add(new TaskTriggerCommand($container)); $application->add(new CronjobCommand($container)); +$application->add(new ResetPasswordCommand($container)); +$application->add(new ResetTwoFactorCommand($container)); $application->run(); -- cgit v1.2.3