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) --- tests/units/Core/TemplateTest.php | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) (limited to 'tests/units') 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 820c929ab38273c80d0930e2e6140dd7676ba4df Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 26 Mar 2016 14:43:41 -0400 Subject: Added avatar image upload --- app/Controller/AvatarFile.php | 55 ++++++++ app/Controller/User.php | 37 ++++++ app/Core/Base.php | 1 + app/Core/Http/Response.php | 18 +++ app/Core/Thumbnail.php | 172 +++++++++++++++++++++++++ app/Core/Tool.php | 74 ----------- app/Core/User/Avatar/AvatarManager.php | 7 +- app/Core/User/UserSession.php | 13 ++ app/Helper/AvatarHelper.php | 22 ++-- app/Model/AvatarFile.php | 111 ++++++++++++++++ app/Model/Comment.php | 3 +- app/Model/File.php | 19 +-- app/Model/ProjectActivity.php | 6 +- app/Model/TaskFinder.php | 1 + app/Model/User.php | 10 +- app/Schema/Mysql.php | 7 +- app/Schema/Postgres.php | 7 +- app/Schema/Sqlite.php | 7 +- app/ServiceProvider/AuthenticationProvider.php | 1 + app/ServiceProvider/AvatarProvider.php | 2 + app/ServiceProvider/ClassProvider.php | 1 + app/Template/board/task_avatar.php | 1 + app/Template/comment/show.php | 2 +- app/Template/event/events.php | 3 +- app/Template/user/avatar.php | 20 +++ app/Template/user/sidebar.php | 3 + app/User/Avatar/AvatarFileProvider.php | 42 ++++++ app/User/Avatar/GravatarProvider.php | 5 +- app/User/Avatar/LetterAvatarProvider.php | 5 +- tests/units/Model/ProjectFileTest.php | 4 +- tests/units/Model/TaskFileTest.php | 4 +- 31 files changed, 548 insertions(+), 115 deletions(-) create mode 100644 app/Controller/AvatarFile.php create mode 100644 app/Core/Thumbnail.php create mode 100644 app/Model/AvatarFile.php create mode 100644 app/Template/user/avatar.php create mode 100644 app/User/Avatar/AvatarFileProvider.php (limited to 'tests/units') diff --git a/app/Controller/AvatarFile.php b/app/Controller/AvatarFile.php new file mode 100644 index 00000000..f8298e16 --- /dev/null +++ b/app/Controller/AvatarFile.php @@ -0,0 +1,55 @@ +request->getIntegerParam('user_id'); + $size = $this->request->getStringParam('size', 48); + $filename = $this->avatarFile->getFilename($user_id); + $etag = md5($filename.$size); + + $this->response->cache(365 * 86400, $etag); + $this->response->contentType('image/jpeg'); + + if ($this->request->getHeader('If-None-Match') !== '"'.$etag.'"') { + $this->render($filename, $size); + } else { + $this->response->status(304); + } + } + + /** + * Render thumbnail from object storage + * + * @access private + * @param string $filename + * @param integer $size + */ + private function render($filename, $size) + { + try { + $blob = $this->objectStorage->get($filename); + + Thumbnail::createFromString($blob) + ->resize($size, $size) + ->toOutput(); + } catch (ObjectStorageException $e) { + $this->logger->error($e->getMessage()); + } + } +} diff --git a/app/Controller/User.php b/app/Controller/User.php index f7d7d2e0..8c02ef7f 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -405,4 +405,41 @@ class User extends Base 'user' => $user, ))); } + + /** + * Display avatar page + */ + public function avatar() + { + $user = $this->getUser(); + + $this->response->html($this->helper->layout->user('user/avatar', array( + 'user' => $user, + ))); + } + + /** + * Upload Avatar + */ + public function uploadAvatar() + { + $user = $this->getUser(); + + if (! $this->avatarFile->uploadFile($user['id'], $this->request->getFileInfo('avatar'))) { + $this->flash->failure(t('Unable to upload the file.')); + } + + $this->response->redirect($this->helper->url->to('user', 'avatar', array('user_id' => $user['id']))); + } + + /** + * Remove Avatar image + */ + public function removeAvatar() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + $this->avatarFile->remove($user['id']); + $this->response->redirect($this->helper->url->to('user', 'avatar', array('user_id' => $user['id']))); + } } diff --git a/app/Core/Base.php b/app/Core/Base.php index f87f271a..74573e94 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -60,6 +60,7 @@ use Pimple\Container; * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter * @property \Kanboard\Model\Action $action * @property \Kanboard\Model\ActionParameter $actionParameter + * @property \Kanboard\Model\AvatarFile $avatarFile * @property \Kanboard\Model\Board $board * @property \Kanboard\Model\Category $category * @property \Kanboard\Model\Color $color diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index d098f519..37349ca5 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -13,6 +13,24 @@ use Kanboard\Core\Csv; */ class Response extends Base { + /** + * Send headers to cache a resource + * + * @access public + * @param integer $duration + * @param string $etag + */ + public function cache($duration, $etag = '') + { + header('Pragma: cache'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT'); + header('Cache-Control: public, max-age=' . $duration); + + if ($etag) { + header('ETag: "' . $etag . '"'); + } + } + /** * Send no cache headers * diff --git a/app/Core/Thumbnail.php b/app/Core/Thumbnail.php new file mode 100644 index 00000000..733d3a3c --- /dev/null +++ b/app/Core/Thumbnail.php @@ -0,0 +1,172 @@ +fromFile($filename); + return $self; + } + + /** + * Create a thumbnail from a string + * + * @static + * @access public + * @param string $blob + * @return Thumbnail + */ + public static function createFromString($blob) + { + $self = new static(); + $self->fromString($blob); + return $self; + } + + /** + * Load the local image file in memory with GD + * + * @access public + * @param string $filename + * @return Thumbnail + */ + public function fromFile($filename) + { + $this->metadata = getimagesize($filename); + $this->srcImage = imagecreatefromstring(file_get_contents($filename)); + return $this; + } + + /** + * Load the image blob in memory with GD + * + * @access public + * @param string $blob + * @return Thumbnail + */ + public function fromString($blob) + { + if (!function_exists('getimagesizefromstring')) { + $uri = 'data://application/octet-stream;base64,' . base64_encode($blob); + $this->metadata = getimagesize($uri); + } else { + $this->metadata = getimagesizefromstring($blob); + } + + $this->srcImage = imagecreatefromstring($blob); + return $this; + } + + /** + * Resize the image + * + * @access public + * @param int $width + * @param int $height + * @return Thumbnail + */ + public function resize($width = 250, $height = 100) + { + $srcWidth = $this->metadata[0]; + $srcHeight = $this->metadata[1]; + $dstX = 0; + $dstY = 0; + + if ($width == 0 && $height == 0) { + $width = 100; + $height = 100; + } + + if ($width > 0 && $height == 0) { + $dstWidth = $width; + $dstHeight = floor($srcHeight * ($width / $srcWidth)); + $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight); + } elseif ($width == 0 && $height > 0) { + $dstWidth = floor($srcWidth * ($height / $srcHeight)); + $dstHeight = $height; + $this->dstImage = imagecreatetruecolor($dstWidth, $dstHeight); + } else { + $srcRatio = $srcWidth / $srcHeight; + $resizeRatio = $width / $height; + + if ($srcRatio <= $resizeRatio) { + $dstWidth = $width; + $dstHeight = floor($srcHeight * ($width / $srcWidth)); + $dstY = ($dstHeight - $height) / 2 * (-1); + } else { + $dstWidth = floor($srcWidth * ($height / $srcHeight)); + $dstHeight = $height; + $dstX = ($dstWidth - $width) / 2 * (-1); + } + + $this->dstImage = imagecreatetruecolor($width, $height); + } + + imagecopyresampled($this->dstImage, $this->srcImage, $dstX, $dstY, 0, 0, $dstWidth, $dstHeight, $srcWidth, $srcHeight); + + return $this; + } + + /** + * Save the thumbnail to a local file + * + * @access public + * @param string $filename + * @return Thumbnail + */ + public function toFile($filename) + { + imagejpeg($this->dstImage, $filename); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + return $this; + } + + /** + * Return the thumbnail as a string + * + * @access public + * @return string + */ + public function toString() + { + ob_start(); + imagejpeg($this->dstImage, null); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + return ob_get_clean(); + } + + /** + * Output the thumbnail directly to the browser or stdout + * + * @access public + */ + public function toOutput() + { + imagejpeg($this->dstImage, null); + imagedestroy($this->dstImage); + imagedestroy($this->srcImage); + } +} diff --git a/app/Core/Tool.php b/app/Core/Tool.php index db2445a1..3423998d 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -75,78 +75,4 @@ class Tool return $container; } - - /** - * Generate a jpeg thumbnail from an image - * - * @static - * @access public - * @param string $src_file Source file image - * @param string $dst_file Destination file image - * @param integer $resize_width Desired image width - * @param integer $resize_height Desired image height - */ - public static function generateThumbnail($src_file, $dst_file, $resize_width = 250, $resize_height = 100) - { - $metadata = getimagesize($src_file); - $src_width = $metadata[0]; - $src_height = $metadata[1]; - $dst_y = 0; - $dst_x = 0; - - if (empty($metadata['mime'])) { - return; - } - - if ($resize_width == 0 && $resize_height == 0) { - $resize_width = 100; - $resize_height = 100; - } - - if ($resize_width > 0 && $resize_height == 0) { - $dst_width = $resize_width; - $dst_height = floor($src_height * ($resize_width / $src_width)); - $dst_image = imagecreatetruecolor($dst_width, $dst_height); - } elseif ($resize_width == 0 && $resize_height > 0) { - $dst_width = floor($src_width * ($resize_height / $src_height)); - $dst_height = $resize_height; - $dst_image = imagecreatetruecolor($dst_width, $dst_height); - } else { - $src_ratio = $src_width / $src_height; - $resize_ratio = $resize_width / $resize_height; - - if ($src_ratio <= $resize_ratio) { - $dst_width = $resize_width; - $dst_height = floor($src_height * ($resize_width / $src_width)); - - $dst_y = ($dst_height - $resize_height) / 2 * (-1); - } else { - $dst_width = floor($src_width * ($resize_height / $src_height)); - $dst_height = $resize_height; - - $dst_x = ($dst_width - $resize_width) / 2 * (-1); - } - - $dst_image = imagecreatetruecolor($resize_width, $resize_height); - } - - switch ($metadata['mime']) { - case 'image/jpeg': - case 'image/jpg': - $src_image = imagecreatefromjpeg($src_file); - break; - case 'image/png': - $src_image = imagecreatefrompng($src_file); - break; - case 'image/gif': - $src_image = imagecreatefromgif($src_file); - break; - default: - return; - } - - imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height); - imagejpeg($dst_image, $dst_file); - imagedestroy($dst_image); - } } diff --git a/app/Core/User/Avatar/AvatarManager.php b/app/Core/User/Avatar/AvatarManager.php index 71bd8aa5..5b61cbdb 100644 --- a/app/Core/User/Avatar/AvatarManager.php +++ b/app/Core/User/Avatar/AvatarManager.php @@ -32,23 +32,25 @@ class AvatarManager } /** - * Render avatar html element + * Render avatar HTML element * * @access public * @param string $user_id * @param string $username * @param string $name * @param string $email + * @param string $avatar_path * @param int $size * @return string */ - public function render($user_id, $username, $name, $email, $size) + public function render($user_id, $username, $name, $email, $avatar_path, $size) { $user = array( 'id' => $user_id, 'username' => $username, 'name' => $name, 'email' => $email, + 'avatar_path' => $avatar_path, ); krsort($this->providers); @@ -80,6 +82,7 @@ class AvatarManager 'username' => '', 'name' => '?', 'email' => '', + 'avatar_path' => '', ); return $provider->render($user, $size); diff --git a/app/Core/User/UserSession.php b/app/Core/User/UserSession.php index e494e7b4..0034c47a 100644 --- a/app/Core/User/UserSession.php +++ b/app/Core/User/UserSession.php @@ -13,6 +13,19 @@ use Kanboard\Core\Security\Role; */ class UserSession extends Base { + /** + * Refresh current session if necessary + * + * @access public + * @param integer $user_id + */ + public function refresh($user_id) + { + if ($this->getId() == $user_id) { + $this->initialize($this->user->getById($user_id)); + } + } + /** * Update user session * diff --git a/app/Helper/AvatarHelper.php b/app/Helper/AvatarHelper.php index c4e27ed9..a36d9b4a 100644 --- a/app/Helper/AvatarHelper.php +++ b/app/Helper/AvatarHelper.php @@ -20,16 +20,17 @@ class AvatarHelper extends Base * @param string $username * @param string $name * @param string $email + * @param string $avatar_path * @param string $css * @param int $size * @return string */ - public function render($user_id, $username, $name, $email, $css = 'avatar-left', $size = 48) + public function render($user_id, $username, $name, $email, $avatar_path, $css = 'avatar-left', $size = 48) { if (empty($user_id) && empty($username)) { $html = $this->avatarManager->renderDefault($size); } else { - $html = $this->avatarManager->render($user_id, $username, $name, $email, $size); + $html = $this->avatarManager->render($user_id, $username, $name, $email, $avatar_path, $size); } return '
'.$html.'
'; @@ -39,26 +40,29 @@ class AvatarHelper extends Base * Render small user avatar * * @access public - * @param string $user_id - * @param string $username - * @param string $name - * @param string $email + * @param string $user_id + * @param string $username + * @param string $name + * @param string $email + * @param string $avatar_path + * @param string $css * @return string */ - public function small($user_id, $username, $name, $email, $css = '') + public function small($user_id, $username, $name, $email, $avatar_path, $css = '') { - return $this->render($user_id, $username, $name, $email, $css, 20); + return $this->render($user_id, $username, $name, $email, $avatar_path, $css, 20); } /** * Get a small avatar for the current user * * @access public + * @param string $css * @return string */ public function currentUserSmall($css = '') { $user = $this->userSession->getAll(); - return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $css); + return $this->small($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css); } } diff --git a/app/Model/AvatarFile.php b/app/Model/AvatarFile.php new file mode 100644 index 00000000..52d07962 --- /dev/null +++ b/app/Model/AvatarFile.php @@ -0,0 +1,111 @@ +db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('avatar_path'); + } + + /** + * Add avatar in the user profile + * + * @access public + * @param integer $user_id Foreign key + * @param string $path Path on the disk + * @return bool + */ + public function create($user_id, $path) + { + $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + 'avatar_path' => $path, + )); + + $this->userSession->refresh($user_id); + + return $result; + } + + /** + * Remove avatar from the user profile + * + * @access public + * @param integer $user_id Foreign key + * @return bool + */ + public function remove($user_id) + { + try { + $this->objectStorage->remove($this->getFilename($user_id)); + $result = $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('avatar_path' => '')); + $this->userSession->refresh($user_id); + return $result; + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + return false; + } + } + + /** + * Upload avatar image + * + * @access public + * @param integer $user_id + * @param array $file + */ + public function uploadFile($user_id, array $file) + { + try { + if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) { + $destination_filename = $this->generatePath($user_id, $file['name']); + $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename); + $this->create($user_id, $destination_filename); + } else { + throw new Exception('File not uploaded: '.var_export($file['error'], true)); + } + + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + return false; + } + + return true; + } + + /** + * Generate the path for a new filename + * + * @access public + * @param integer $user_id + * @param string $filename + * @return string + */ + public function generatePath($user_id, $filename) + { + return implode(DIRECTORY_SEPARATOR, array(self::PATH_PREFIX, $user_id, hash('sha1', $filename.time()))); + } +} diff --git a/app/Model/Comment.php b/app/Model/Comment.php index 6eb4a1e5..f7ac4eaa 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -48,7 +48,8 @@ class Comment extends Base self::TABLE.'.comment', User::TABLE.'.username', User::TABLE.'.name', - User::TABLE.'.email' + User::TABLE.'.email', + User::TABLE.'.avatar_path' ) ->join(User::TABLE, 'id', 'user_id') ->orderBy(self::TABLE.'.date_creation', $sorting) diff --git a/app/Model/File.php b/app/Model/File.php index 03ea691d..5e77060c 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -3,6 +3,7 @@ namespace Kanboard\Model; use Exception; +use Kanboard\Core\Thumbnail; use Kanboard\Event\FileEvent; use Kanboard\Core\Tool; use Kanboard\Core\ObjectStorage\ObjectStorageException; @@ -315,15 +316,15 @@ abstract class File extends Base */ public function generateThumbnailFromData($destination_filename, &$data) { - $temp_filename = tempnam(sys_get_temp_dir(), 'datafile'); + $blob = Thumbnail::createFromString($data) + ->resize() + ->toString(); - file_put_contents($temp_filename, $data); - $this->generateThumbnailFromFile($temp_filename, $destination_filename); - unlink($temp_filename); + $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob); } /** - * Generate thumbnail from a blob + * Generate thumbnail from a local file * * @access public * @param string $uploaded_filename @@ -331,8 +332,10 @@ abstract class File extends Base */ public function generateThumbnailFromFile($uploaded_filename, $destination_filename) { - $thumbnail_filename = tempnam(sys_get_temp_dir(), 'thumbnail'); - Tool::generateThumbnail($uploaded_filename, $thumbnail_filename); - $this->objectStorage->moveFile($thumbnail_filename, $this->getThumbnailPath($destination_filename)); + $blob = Thumbnail::createFromFile($uploaded_filename) + ->resize() + ->toString(); + + $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob); } } diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 74df26a1..d399d5c6 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -88,7 +88,8 @@ class ProjectActivity extends Base self::TABLE.'.*', User::TABLE.'.username AS author_username', User::TABLE.'.name AS author_name', - User::TABLE.'.email' + User::TABLE.'.email', + User::TABLE.'.avatar_path' ) ->in('project_id', $project_ids) ->join(User::TABLE, 'id', 'creator_id') @@ -117,7 +118,8 @@ class ProjectActivity extends Base self::TABLE.'.*', User::TABLE.'.username AS author_username', User::TABLE.'.name AS author_name', - User::TABLE.'.email' + User::TABLE.'.email', + User::TABLE.'.avatar_path' ) ->eq('task_id', $task_id) ->join(User::TABLE, 'id', 'creator_id') diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index d67372cc..7bca2284 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -128,6 +128,7 @@ class TaskFinder extends Base User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name', User::TABLE.'.email AS assignee_email', + User::TABLE.'.avatar_path AS assignee_avatar_path', Category::TABLE.'.name AS category_name', Category::TABLE.'.description AS category_description', Column::TABLE.'.title AS column_name', diff --git a/app/Model/User.php b/app/Model/User.php index 0e11422b..b093d55f 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -283,12 +283,7 @@ class User extends Base { $this->prepare($values); $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); - - // If the user is connected refresh his session - if ($this->userSession->getId() == $values['id']) { - $this->userSession->initialize($this->getById($this->userSession->getId())); - } - + $this->userSession->refresh($values['id']); return $result; } @@ -327,6 +322,9 @@ class User extends Base { return $this->db->transaction(function (Database $db) use ($user_id) { + // Remove Avatar + $this->avatarFile->remove($user_id); + // All assigned tasks are now unassigned (no foreign key) if (! $db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) { return false; diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 9bfe6649..ccb5a9cf 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 108; +const VERSION = 109; + +function version_109(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)"); +} function version_108(PDO $pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 28c563de..3ef49498 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,12 @@ use PDO; use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; -const VERSION = 88; +const VERSION = 89; + +function version_89(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path VARCHAR(255)"); +} function version_88(PDO $pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index c6f60332..9ded7ed9 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,12 @@ use Kanboard\Core\Security\Token; use Kanboard\Core\Security\Role; use PDO; -const VERSION = 100; +const VERSION = 101; + +function version_101(PDO $pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN avatar_path TEXT"); +} function version_100(PDO $pdo) { diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php index d59ffd9e..776e65d5 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -125,6 +125,7 @@ class AuthenticationProvider implements ServiceProviderInterface $acl->add('Board', 'readonly', Role::APP_PUBLIC); $acl->add('Ical', '*', Role::APP_PUBLIC); $acl->add('Feed', '*', Role::APP_PUBLIC); + $acl->add('AvatarFile', 'show', Role::APP_PUBLIC); $acl->add('Config', '*', Role::APP_ADMIN); $acl->add('Currency', '*', Role::APP_ADMIN); diff --git a/app/ServiceProvider/AvatarProvider.php b/app/ServiceProvider/AvatarProvider.php index 73d37d5c..aac4fcab 100644 --- a/app/ServiceProvider/AvatarProvider.php +++ b/app/ServiceProvider/AvatarProvider.php @@ -6,6 +6,7 @@ use Pimple\Container; use Pimple\ServiceProviderInterface; use Kanboard\Core\User\Avatar\AvatarManager; use Kanboard\User\Avatar\GravatarProvider; +use Kanboard\User\Avatar\AvatarFileProvider; use Kanboard\User\Avatar\LetterAvatarProvider; /** @@ -28,6 +29,7 @@ class AvatarProvider implements ServiceProviderInterface $container['avatarManager'] = new AvatarManager; $container['avatarManager']->register(new LetterAvatarProvider($container)); $container['avatarManager']->register(new GravatarProvider($container)); + $container['avatarManager']->register(new AvatarFileProvider($container)); return $container; } } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index b883c905..3e654a4e 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface 'Model' => array( 'Action', 'ActionParameter', + 'AvatarFile', 'Board', 'Category', 'Color', diff --git a/app/Template/board/task_avatar.php b/app/Template/board/task_avatar.php index 5630c190..39f6b54d 100644 --- a/app/Template/board/task_avatar.php +++ b/app/Template/board/task_avatar.php @@ -12,6 +12,7 @@ $task['assignee_username'], $task['assignee_name'], $task['assignee_email'], + $task['assignee_avatar_path'], 'avatar-inline' ) ?> diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php index ce456c5d..3f45e2e7 100644 --- a/app/Template/comment/show.php +++ b/app/Template/comment/show.php @@ -1,6 +1,6 @@
- avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email']) ?> + avatar->render($comment['user_id'], $comment['username'], $comment['name'], $comment['email'], $comment['avatar_path']) ?>
diff --git a/app/Template/event/events.php b/app/Template/event/events.php index ef651321..c58376c4 100644 --- a/app/Template/event/events.php +++ b/app/Template/event/events.php @@ -7,7 +7,8 @@ $event['creator_id'], $event['author_username'], $event['author_name'], - $event['email'] + $event['email'], + $event['avatar_path'] ) ?>
diff --git a/app/Template/user/avatar.php b/app/Template/user/avatar.php new file mode 100644 index 00000000..c285f44d --- /dev/null +++ b/app/Template/user/avatar.php @@ -0,0 +1,20 @@ + + +avatar->render($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], '') ?> + +
+ form->csrf() ?> + form->label(t('Upload my avatar image'), 'avatar') ?> + form->file('avatar') ?> + +
+ + url->link(t('Remove my image'), 'User', 'removeAvatar', array('user_id' => $user['id']), true, 'btn btn-red') ?> + + + + url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> +
+
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php index 20fd2ad2..ecadc60d 100644 --- a/app/Template/user/sidebar.php +++ b/app/Template/user/sidebar.php @@ -37,6 +37,9 @@
  • app->checkMenuSelection('user', 'edit') ?>> url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
  • +
  • app->checkMenuSelection('user', 'avatar') ?>> + url->link(t('Avatar'), 'user', 'avatar', array('user_id' => $user['id'])) ?> +
  • diff --git a/app/User/Avatar/AvatarFileProvider.php b/app/User/Avatar/AvatarFileProvider.php new file mode 100644 index 00000000..87a42c07 --- /dev/null +++ b/app/User/Avatar/AvatarFileProvider.php @@ -0,0 +1,42 @@ +helper->url->href('AvatarFile', 'show', array('user_id' => $user['id'], 'size' => $size)); + $title = $this->helper->text->e($user['name'] ?: $user['username']); + return '' . $title . ''; + } + + /** + * Determine if the provider is active + * + * @access public + * @param array $user + * @return boolean + */ + public function isActive(array $user) + { + return !empty($user['avatar_path']); + } +} diff --git a/app/User/Avatar/GravatarProvider.php b/app/User/Avatar/GravatarProvider.php index 7a719734..87ca51b1 100644 --- a/app/User/Avatar/GravatarProvider.php +++ b/app/User/Avatar/GravatarProvider.php @@ -17,8 +17,9 @@ class GravatarProvider extends Base implements AvatarProviderInterface * Render avatar html * * @access public - * @param array $user - * @param int $size + * @param array $user + * @param int $size + * @return string */ public function render(array $user, $size) { diff --git a/app/User/Avatar/LetterAvatarProvider.php b/app/User/Avatar/LetterAvatarProvider.php index 81c4586d..cf04f2a7 100644 --- a/app/User/Avatar/LetterAvatarProvider.php +++ b/app/User/Avatar/LetterAvatarProvider.php @@ -24,8 +24,9 @@ class LetterAvatarProvider extends Base implements AvatarProviderInterface * Render avatar html * * @access public - * @param array $user - * @param int $size + * @param array $user + * @param int $size + * @return string */ public function render(array $user, $size) { diff --git a/tests/units/Model/ProjectFileTest.php b/tests/units/Model/ProjectFileTest.php index d9b37fbe..0d7a9377 100644 --- a/tests/units/Model/ProjectFileTest.php +++ b/tests/units/Model/ProjectFileTest.php @@ -278,7 +278,7 @@ class ProjectFileTest extends Base $fileModel = $this ->getMockBuilder('\Kanboard\Model\ProjectFile') ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromFile')) + ->setMethods(array('generateThumbnailFromData')) ->getMock(); $projectModel = new Project($this->container); @@ -288,7 +288,7 @@ class ProjectFileTest extends Base $fileModel ->expects($this->once()) - ->method('generateThumbnailFromFile'); + ->method('generateThumbnailFromData'); $this->container['objectStorage'] ->expects($this->once()) diff --git a/tests/units/Model/TaskFileTest.php b/tests/units/Model/TaskFileTest.php index b900e8f3..e44e092d 100644 --- a/tests/units/Model/TaskFileTest.php +++ b/tests/units/Model/TaskFileTest.php @@ -331,7 +331,7 @@ class TaskFileTest extends Base $fileModel = $this ->getMockBuilder('\Kanboard\Model\TaskFile') ->setConstructorArgs(array($this->container)) - ->setMethods(array('generateThumbnailFromFile')) + ->setMethods(array('generateThumbnailFromData')) ->getMock(); $projectModel = new Project($this->container); @@ -343,7 +343,7 @@ class TaskFileTest extends Base $fileModel ->expects($this->once()) - ->method('generateThumbnailFromFile'); + ->method('generateThumbnailFromData'); $this->container['objectStorage'] ->expects($this->once()) -- 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 'tests/units') 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 9ba44a01dbb187f4c931e1ba838e2bad258d34f4 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 13:45:37 -0400 Subject: Minor improvements --- app/Core/Helper.php | 1 + app/Core/Template.php | 15 +++++++++++++++ tests/units/Base.php | 1 + 3 files changed, 17 insertions(+) (limited to 'tests/units') diff --git a/app/Core/Helper.php b/app/Core/Helper.php index e6152127..3a66fbd0 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -15,6 +15,7 @@ use Pimple\Container; * @property \Kanboard\Helper\DateHelper $dt * @property \Kanboard\Helper\FileHelper $file * @property \Kanboard\Helper\FormHelper $form + * @property \Kanboard\Helper\HookHelper $hook * @property \Kanboard\Helper\ModelHelper $model * @property \Kanboard\Helper\SubtaskHelper $subtask * @property \Kanboard\Helper\TaskHelper $task diff --git a/app/Core/Template.php b/app/Core/Template.php index cf5512d9..1874d44a 100644 --- a/app/Core/Template.php +++ b/app/Core/Template.php @@ -7,6 +7,21 @@ namespace Kanboard\Core; * * @package core * @author Frederic Guillot + * + * @property \Kanboard\Helper\AppHelper $app + * @property \Kanboard\Helper\AssetHelper $asset + * @property \Kanboard\Helper\DateHelper $dt + * @property \Kanboard\Helper\FileHelper $file + * @property \Kanboard\Helper\FormHelper $form + * @property \Kanboard\Helper\HookHelper $hook + * @property \Kanboard\Helper\ModelHelper $model + * @property \Kanboard\Helper\SubtaskHelper $subtask + * @property \Kanboard\Helper\TaskHelper $task + * @property \Kanboard\Helper\TextHelper $text + * @property \Kanboard\Helper\UrlHelper $url + * @property \Kanboard\Helper\UserHelper $user + * @property \Kanboard\Helper\LayoutHelper $layout + * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader */ class Template { diff --git a/tests/units/Base.php b/tests/units/Base.php index 6af14ba5..563035f6 100644 --- a/tests/units/Base.php +++ b/tests/units/Base.php @@ -39,6 +39,7 @@ abstract class Base extends PHPUnit_Framework_TestCase $this->container->register(new Kanboard\ServiceProvider\ClassProvider); $this->container->register(new Kanboard\ServiceProvider\NotificationProvider); $this->container->register(new Kanboard\ServiceProvider\RouteProvider); + $this->container->register(new Kanboard\ServiceProvider\AvatarProvider); $this->container['dispatcher'] = new TraceableEventDispatcher( new EventDispatcher, -- 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 'tests/units') 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 a20f4f2904f12e6b90aac2efdc5be3472cd74375 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 27 Mar 2016 17:20:21 -0400 Subject: Improve UserHelper::getInitials() --- app/Helper/UserHelper.php | 2 +- tests/units/Helper/UserHelperTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'tests/units') diff --git a/app/Helper/UserHelper.php b/app/Helper/UserHelper.php index ee7d8ba5..c3369dfd 100644 --- a/app/Helper/UserHelper.php +++ b/app/Helper/UserHelper.php @@ -34,7 +34,7 @@ class UserHelper extends Base { $initials = ''; - foreach (explode(' ', $name) as $string) { + foreach (explode(' ', $name, 2) as $string) { $initials .= mb_substr($string, 0, 1); } diff --git a/tests/units/Helper/UserHelperTest.php b/tests/units/Helper/UserHelperTest.php index 7ee6e8bb..9a9832b2 100644 --- a/tests/units/Helper/UserHelperTest.php +++ b/tests/units/Helper/UserHelperTest.php @@ -15,6 +15,7 @@ class UserHelperTest extends Base $helper = new UserHelper($this->container); $this->assertEquals('CN', $helper->getInitials('chuck norris')); + $this->assertEquals('CN', $helper->getInitials('chuck norris #2')); $this->assertEquals('A', $helper->getInitials('admin')); } -- cgit v1.2.3 From d8027c58d5bb5c2ac0dcce9e12370927f2699e28 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Tue, 29 Mar 2016 21:58:17 -0400 Subject: Fix broken unit test in PR #2066 --- tests/units/User/Avatar/LetterAvatarProviderTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tests/units') diff --git a/tests/units/User/Avatar/LetterAvatarProviderTest.php b/tests/units/User/Avatar/LetterAvatarProviderTest.php index 0c1bfc4b..39e51c98 100644 --- a/tests/units/User/Avatar/LetterAvatarProviderTest.php +++ b/tests/units/User/Avatar/LetterAvatarProviderTest.php @@ -23,7 +23,7 @@ class LetterAvatarProviderTest extends Base { $provider = new LetterAvatarProvider($this->container); $user = array('id' => 123, 'name' => 'Kanboard Admin', 'username' => 'bob', 'email' => ''); - $expected = '
    KA
    '; + $expected = '
    KA
    '; $this->assertEquals($expected, $provider->render($user, 48)); } @@ -31,7 +31,7 @@ class LetterAvatarProviderTest extends Base { $provider = new LetterAvatarProvider($this->container); $user = array('id' => 123, 'name' => '', 'username' => 'admin', 'email' => ''); - $expected = '
    A
    '; + $expected = '
    A
    '; $this->assertEquals($expected, $provider->render($user, 48)); } } -- 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 'tests/units') 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 'tests/units') 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 'tests/units') 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 2eadfb22912d94e76a479b694070735fbb0298f1 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 10 Apr 2016 12:13:42 -0400 Subject: Refactor ProjectActivity model to use Filter and Formatter interface --- app/Api/Me.php | 2 +- app/Api/Project.php | 4 +- app/Controller/Activity.php | 4 +- app/Controller/App.php | 2 +- app/Controller/Feed.php | 4 +- app/Controller/ProjectOverview.php | 2 +- app/Core/Base.php | 2 + app/Core/Helper.php | 1 + app/Filter/ProjectActivityProjectIdFilter.php | 38 ++++++ app/Filter/ProjectActivityProjectIdsFilter.php | 43 ++++++ app/Filter/ProjectActivityTaskIdFilter.php | 38 ++++++ app/Filter/ProjectActivityTaskTitleFilter.php | 38 ++++++ app/Formatter/ProjectActivityEventFormatter.php | 61 +++++++++ app/Helper/ProjectActivityHelper.php | 78 +++++++++++ app/Model/ProjectActivity.php | 151 ++------------------- app/ServiceProvider/FilterProvider.php | 42 +++++- app/ServiceProvider/HelperProvider.php | 1 + .../Filter/ProjectActivityProjectIdFilterTest.php | 35 +++++ .../Filter/ProjectActivityProjectIdsFilterTest.php | 63 +++++++++ .../Filter/ProjectActivityTaskIdFilterTest.php | 34 +++++ .../Filter/ProjectActivityTaskTitleFilterTest.php | 34 +++++ tests/units/Helper/ProjectActivityHelperTest.php | 97 +++++++++++++ tests/units/Model/ProjectActivityTest.php | 83 +++-------- 23 files changed, 647 insertions(+), 210 deletions(-) create mode 100644 app/Filter/ProjectActivityProjectIdFilter.php create mode 100644 app/Filter/ProjectActivityProjectIdsFilter.php create mode 100644 app/Filter/ProjectActivityTaskIdFilter.php create mode 100644 app/Filter/ProjectActivityTaskTitleFilter.php create mode 100644 app/Formatter/ProjectActivityEventFormatter.php create mode 100644 app/Helper/ProjectActivityHelper.php create mode 100644 tests/units/Filter/ProjectActivityProjectIdFilterTest.php create mode 100644 tests/units/Filter/ProjectActivityProjectIdsFilterTest.php create mode 100644 tests/units/Filter/ProjectActivityTaskIdFilterTest.php create mode 100644 tests/units/Filter/ProjectActivityTaskTitleFilterTest.php create mode 100644 tests/units/Helper/ProjectActivityHelperTest.php (limited to 'tests/units') diff --git a/app/Api/Me.php b/app/Api/Me.php index ccc809ed..3d08626a 100644 --- a/app/Api/Me.php +++ b/app/Api/Me.php @@ -33,7 +33,7 @@ class Me extends Base public function getMyActivityStream() { $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId()); - return $this->projectActivity->getProjects($project_ids, 100); + return $this->helper->projectActivity->getProjectsEvents($project_ids, 100); } public function createMyPrivateProject($name, $description = null) diff --git a/app/Api/Project.php b/app/Api/Project.php index 8e311f7f..846d7046 100644 --- a/app/Api/Project.php +++ b/app/Api/Project.php @@ -53,13 +53,13 @@ class Project extends Base public function getProjectActivities(array $project_ids) { - return $this->projectActivity->getProjects($project_ids); + return $this->helper->projectActivity->getProjectsEvents($project_ids); } public function getProjectActivity($project_id) { $this->checkProjectPermission($project_id); - return $this->projectActivity->getProject($project_id); + return $this->helper->projectActivity->getProjectEvents($project_id); } public function createProject($name, $description = null) diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php index e455b1da..47a66e0a 100644 --- a/app/Controller/Activity.php +++ b/app/Controller/Activity.php @@ -20,7 +20,7 @@ class Activity extends Base $project = $this->getProject(); $this->response->html($this->helper->layout->app('activity/project', array( - 'events' => $this->projectActivity->getProject($project['id']), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id']), 'project' => $project, 'title' => t('%s\'s activity', $project['name']) ))); @@ -39,7 +39,7 @@ class Activity extends Base 'title' => $task['title'], 'task' => $task, 'project' => $this->project->getById($task['project_id']), - 'events' => $this->projectActivity->getTask($task['id']), + 'events' => $this->helper->projectActivity->getTaskEvents($task['id']), ))); } } diff --git a/app/Controller/App.php b/app/Controller/App.php index df1d3c90..01f733ff 100644 --- a/app/Controller/App.php +++ b/app/Controller/App.php @@ -157,7 +157,7 @@ class App extends Base $this->response->html($this->helper->layout->dashboard('app/activity', array( 'title' => t('My activity stream'), - 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id']), 100), + 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id']), 100), 'user' => $user, ))); } diff --git a/app/Controller/Feed.php b/app/Controller/Feed.php index 8457c383..f8b3d320 100644 --- a/app/Controller/Feed.php +++ b/app/Controller/Feed.php @@ -26,7 +26,7 @@ class Feed extends Base } $this->response->xml($this->template->render('feed/user', array( - 'events' => $this->projectActivity->getProjects($this->projectPermission->getActiveProjectIds($user['id'])), + 'events' => $this->helper->projectActivity->getProjectsEvents($this->projectPermission->getActiveProjectIds($user['id'])), 'user' => $user, ))); } @@ -47,7 +47,7 @@ class Feed extends Base } $this->response->xml($this->template->render('feed/project', array( - 'events' => $this->projectActivity->getProject($project['id']), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id']), 'project' => $project, ))); } diff --git a/app/Controller/ProjectOverview.php b/app/Controller/ProjectOverview.php index 04645804..b2bb33d6 100644 --- a/app/Controller/ProjectOverview.php +++ b/app/Controller/ProjectOverview.php @@ -24,7 +24,7 @@ class ProjectOverview extends Base 'description' => $this->helper->projectHeader->getDescription($project), 'users' => $this->projectUserRole->getAllUsersGroupedByRole($project['id']), 'roles' => $this->role->getProjectRoles(), - 'events' => $this->projectActivity->getProject($project['id'], 10), + 'events' => $this->helper->projectActivity->getProjectEvents($project['id'], 10), 'images' => $this->projectFile->getAllImages($project['id']), 'files' => $this->projectFile->getAllDocuments($project['id']), ))); diff --git a/app/Core/Base.php b/app/Core/Base.php index 8c6b7620..2b619af5 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -129,10 +129,12 @@ use Pimple\Container; * @property \Kanboard\Export\TransitionExport $transitionExport * @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery * @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery + * @property \Kanboard\Core\Filter\QueryBuilder $projectActivityQuery * @property \Kanboard\Core\Filter\QueryBuilder $userQuery * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer + * @property \Kanboard\Core\Filter\LexerBuilder $projectActivityLexer * @property \Psr\Log\LoggerInterface $logger * @property \PicoDb\Database $db * @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher diff --git a/app/Core/Helper.php b/app/Core/Helper.php index ab1f8f76..66f8d429 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -26,6 +26,7 @@ use Pimple\Container; * @property \Kanboard\Helper\UserHelper $user * @property \Kanboard\Helper\LayoutHelper $layout * @property \Kanboard\Helper\ProjectHeaderHelper $projectHeader + * @property \Kanboard\Helper\ProjectActivityHelper $projectActivity */ class Helper { diff --git a/app/Filter/ProjectActivityProjectIdFilter.php b/app/Filter/ProjectActivityProjectIdFilter.php new file mode 100644 index 00000000..bb4d8bd1 --- /dev/null +++ b/app/Filter/ProjectActivityProjectIdFilter.php @@ -0,0 +1,38 @@ +query->eq(ProjectActivity::TABLE.'.project_id', $this->value); + return $this; + } +} diff --git a/app/Filter/ProjectActivityProjectIdsFilter.php b/app/Filter/ProjectActivityProjectIdsFilter.php new file mode 100644 index 00000000..4d7c9028 --- /dev/null +++ b/app/Filter/ProjectActivityProjectIdsFilter.php @@ -0,0 +1,43 @@ +value)) { + $this->query->eq(ProjectActivity::TABLE.'.project_id', 0); + } else { + $this->query->in(ProjectActivity::TABLE.'.project_id', $this->value); + } + + return $this; + } +} diff --git a/app/Filter/ProjectActivityTaskIdFilter.php b/app/Filter/ProjectActivityTaskIdFilter.php new file mode 100644 index 00000000..e99efe09 --- /dev/null +++ b/app/Filter/ProjectActivityTaskIdFilter.php @@ -0,0 +1,38 @@ +query->eq(ProjectActivity::TABLE.'.task_id', $this->value); + return $this; + } +} diff --git a/app/Filter/ProjectActivityTaskTitleFilter.php b/app/Filter/ProjectActivityTaskTitleFilter.php new file mode 100644 index 00000000..ed3f36d6 --- /dev/null +++ b/app/Filter/ProjectActivityTaskTitleFilter.php @@ -0,0 +1,38 @@ +query->ilike(Task::TABLE.'.title', '%'.$this->value.'%'); + return $this; + } +} diff --git a/app/Formatter/ProjectActivityEventFormatter.php b/app/Formatter/ProjectActivityEventFormatter.php new file mode 100644 index 00000000..ae80e5e7 --- /dev/null +++ b/app/Formatter/ProjectActivityEventFormatter.php @@ -0,0 +1,61 @@ +query->findAll(); + + foreach ($events as &$event) { + $event += $this->unserializeEvent($event['data']); + unset($event['data']); + + $event['author'] = $event['author_name'] ?: $event['author_username']; + $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event); + $event['event_content'] = $this->renderEvent($event); + } + + return $events; + } + + /** + * Decode event data, supports unserialize() and json_decode() + * + * @access protected + * @param string $data Serialized data + * @return array + */ + protected function unserializeEvent($data) + { + if ($data{0} === 'a') { + return unserialize($data); + } + + return json_decode($data, true) ?: array(); + } + + /** + * Get the event html content + * + * @access protected + * @param array $params Event properties + * @return string + */ + protected function renderEvent(array $params) + { + return $this->template->render( + 'event/'.str_replace('.', '_', $params['event_name']), + $params + ); + } +} diff --git a/app/Helper/ProjectActivityHelper.php b/app/Helper/ProjectActivityHelper.php new file mode 100644 index 00000000..738fec66 --- /dev/null +++ b/app/Helper/ProjectActivityHelper.php @@ -0,0 +1,78 @@ +projectActivityQuery + ->withFilter(new ProjectActivityProjectIdFilter($project_id)); + + $queryBuilder->getQuery() + ->desc(ProjectActivity::TABLE.'.id') + ->limit($limit) + ; + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } + + /** + * Get projects activity events + * + * @access public + * @param int[] $project_ids + * @param int $limit + * @return array + */ + public function getProjectsEvents(array $project_ids, $limit = 50) + { + $queryBuilder = $this->projectActivityQuery + ->withFilter(new ProjectActivityProjectIdsFilter($project_ids)); + + $queryBuilder->getQuery() + ->desc(ProjectActivity::TABLE.'.id') + ->limit($limit) + ; + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } + + /** + * Get task activity events + * + * @access public + * @param integer $task_id + * @return array + */ + public function getTaskEvents($task_id) + { + $queryBuilder = $this->projectActivityQuery + ->withFilter(new ProjectActivityTaskIdFilter($task_id)); + + $queryBuilder->getQuery()->desc(ProjectActivity::TABLE.'.id'); + + return $queryBuilder->format(new ProjectActivityEventFormatter($this->container)); + } +} diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 34893f0b..31cee113 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -53,115 +53,25 @@ class ProjectActivity extends Base } /** - * Get all events for the given project + * Get query * * @access public - * @param integer $project_id Project id - * @param integer $limit Maximum events number - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - public function getProject($project_id, $limit = 50, $start = null, $end = null) - { - return $this->getProjects(array($project_id), $limit, $start, $end); - } - - /** - * Get all events for the given projects list - * - * @access public - * @param integer[] $project_ids Projects id - * @param integer $limit Maximum events number - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - public function getProjects(array $project_ids, $limit = 50, $start = null, $end = null) - { - if (empty($project_ids)) { - return array(); - } - - $query = $this - ->db - ->table(self::TABLE) - ->columns( - self::TABLE.'.*', - User::TABLE.'.username AS author_username', - User::TABLE.'.name AS author_name', - User::TABLE.'.email', - User::TABLE.'.avatar_path' - ) - ->in('project_id', $project_ids) - ->join(User::TABLE, 'id', 'creator_id') - ->desc(self::TABLE.'.id') - ->limit($limit); - - return $this->getEvents($query, $start, $end); - } - - /** - * Get all events for the given task - * - * @access public - * @param integer $task_id Task id - * @param integer $limit Maximum events number - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array - */ - public function getTask($task_id, $limit = 50, $start = null, $end = null) - { - $query = $this - ->db - ->table(self::TABLE) - ->columns( - self::TABLE.'.*', - User::TABLE.'.username AS author_username', - User::TABLE.'.name AS author_name', - User::TABLE.'.email', - User::TABLE.'.avatar_path' - ) - ->eq('task_id', $task_id) - ->join(User::TABLE, 'id', 'creator_id') - ->desc(self::TABLE.'.id') - ->limit($limit); - - return $this->getEvents($query, $start, $end); - } - - /** - * Common function to return events - * - * @access public - * @param Table $query PicoDb Query - * @param integer $start Timestamp of earliest activity - * @param integer $end Timestamp of latest activity - * @return array + * @return Table */ - private function getEvents(Table $query, $start, $end) + public function getQuery() { - if (! is_null($start)) { - $query->gte('date_creation', $start); - } - - if (! is_null($end)) { - $query->lte('date_creation', $end); - } - - $events = $query->findAll(); - - foreach ($events as &$event) { - $event += $this->decode($event['data']); - unset($event['data']); - - $event['author'] = $event['author_name'] ?: $event['author_username']; - $event['event_title'] = $this->notification->getTitleWithAuthor($event['author'], $event['event_name'], $event); - $event['event_content'] = $this->getContent($event); - } - - return $events; + return $this + ->db + ->table(ProjectActivity::TABLE) + ->columns( + ProjectActivity::TABLE.'.*', + 'uc.username AS author_username', + 'uc.name AS author_name', + 'uc.email', + 'uc.avatar_path' + ) + ->join(Task::TABLE, 'id', 'task_id') + ->left(User::TABLE, 'uc', 'id', ProjectActivity::TABLE, 'creator_id'); } /** @@ -179,35 +89,4 @@ class ProjectActivity extends Base $this->db->table(self::TABLE)->in('id', $ids)->remove(); } } - - /** - * Get the event html content - * - * @access public - * @param array $params Event properties - * @return string - */ - public function getContent(array $params) - { - return $this->template->render( - 'event/'.str_replace('.', '_', $params['event_name']), - $params - ); - } - - /** - * Decode event data, supports unserialize() and json_decode() - * - * @access public - * @param string $data Serialized data - * @return array - */ - public function decode($data) - { - if ($data{0} === 'a') { - return unserialize($data); - } - - return json_decode($data, true) ?: array(); - } } diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php index 3100ae7e..4b4dbd2d 100644 --- a/app/ServiceProvider/FilterProvider.php +++ b/app/ServiceProvider/FilterProvider.php @@ -4,6 +4,7 @@ namespace Kanboard\ServiceProvider; use Kanboard\Core\Filter\LexerBuilder; use Kanboard\Core\Filter\QueryBuilder; +use Kanboard\Filter\ProjectActivityTaskTitleFilter; use Kanboard\Filter\TaskAssigneeFilter; use Kanboard\Filter\TaskCategoryFilter; use Kanboard\Filter\TaskColorFilter; @@ -45,6 +46,25 @@ class FilterProvider implements ServiceProviderInterface * @return \Pimple\Container */ public function register(Container $container) + { + $this->createUserFilter($container); + $this->createProjectFilter($container); + $this->createTaskFilter($container); + return $container; + } + + public function createUserFilter(Container $container) + { + $container['userQuery'] = $container->factory(function ($c) { + $builder = new QueryBuilder(); + $builder->withQuery($c['db']->table(User::TABLE)); + return $builder; + }); + + return $container; + } + + public function createProjectFilter(Container $container) { $container['projectGroupRoleQuery'] = $container->factory(function ($c) { $builder = new QueryBuilder(); @@ -58,18 +78,32 @@ class FilterProvider implements ServiceProviderInterface return $builder; }); - $container['userQuery'] = $container->factory(function ($c) { + $container['projectQuery'] = $container->factory(function ($c) { $builder = new QueryBuilder(); - $builder->withQuery($c['db']->table(User::TABLE)); + $builder->withQuery($c['db']->table(Project::TABLE)); return $builder; }); - $container['projectQuery'] = $container->factory(function ($c) { + $container['projectActivityLexer'] = $container->factory(function ($c) { + $builder = new LexerBuilder(); + $builder->withQuery($c['projectActivity']->getQuery()); + $builder->withFilter(new ProjectActivityTaskTitleFilter()); + + return $builder; + }); + + $container['projectActivityQuery'] = $container->factory(function ($c) { $builder = new QueryBuilder(); - $builder->withQuery($c['db']->table(Project::TABLE)); + $builder->withQuery($c['projectActivity']->getQuery()); + return $builder; }); + return $container; + } + + public function createTaskFilter(Container $container) + { $container['taskQuery'] = $container->factory(function ($c) { $builder = new QueryBuilder(); $builder->withQuery($c['taskFinder']->getExtendedQuery()); diff --git a/app/ServiceProvider/HelperProvider.php b/app/ServiceProvider/HelperProvider.php index 3590afa5..bf3956a2 100644 --- a/app/ServiceProvider/HelperProvider.php +++ b/app/ServiceProvider/HelperProvider.php @@ -30,6 +30,7 @@ class HelperProvider implements ServiceProviderInterface $container['helper']->register('user', '\Kanboard\Helper\UserHelper'); $container['helper']->register('avatar', '\Kanboard\Helper\AvatarHelper'); $container['helper']->register('projectHeader', '\Kanboard\Helper\ProjectHeaderHelper'); + $container['helper']->register('projectActivity', '\Kanboard\Helper\ProjectActivityHelper'); $container['template'] = new Template($container['helper']); diff --git a/tests/units/Filter/ProjectActivityProjectIdFilterTest.php b/tests/units/Filter/ProjectActivityProjectIdFilterTest.php new file mode 100644 index 00000000..193852e1 --- /dev/null +++ b/tests/units/Filter/ProjectActivityProjectIdFilterTest.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 ProjectActivityProjectIdFilter(1); + $filter->withQuery($query)->apply(); + $this->assertCount(1, $query->findAll()); + } +} diff --git a/tests/units/Filter/ProjectActivityProjectIdsFilterTest.php b/tests/units/Filter/ProjectActivityProjectIdsFilterTest.php new file mode 100644 index 00000000..e99d2e2f --- /dev/null +++ b/tests/units/Filter/ProjectActivityProjectIdsFilterTest.php @@ -0,0 +1,63 @@ +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(3, $projectModel->create(array('name' => 'P3'))); + + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2))); + $this->assertEquals(3, $taskCreation->create(array('title' => 'Test', 'project_id' => 3))); + + $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)))); + $this->assertNotFalse($projectActivityModel->createEvent(3, 3, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(3)))); + + $filter = new ProjectActivityProjectIdsFilter(array(1, 2)); + $filter->withQuery($query)->apply(); + $this->assertCount(2, $query->findAll()); + } + + public function testWithEmptyArgument() + { + $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(2, $projectModel->create(array('name' => 'P2'))); + $this->assertEquals(3, $projectModel->create(array('name' => 'P3'))); + + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2))); + $this->assertEquals(3, $taskCreation->create(array('title' => 'Test', 'project_id' => 3))); + + $this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, $taskFinder->getById(1))); + $this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, $taskFinder->getById(2))); + $this->assertNotFalse($projectActivityModel->createEvent(3, 3, 1, Task::EVENT_CREATE, $taskFinder->getById(3))); + + $filter = new ProjectActivityProjectIdsFilter(array()); + $filter->withQuery($query)->apply(); + $this->assertCount(0, $query->findAll()); + } +} diff --git a/tests/units/Filter/ProjectActivityTaskIdFilterTest.php b/tests/units/Filter/ProjectActivityTaskIdFilterTest.php new file mode 100644 index 00000000..646cab1b --- /dev/null +++ b/tests/units/Filter/ProjectActivityTaskIdFilterTest.php @@ -0,0 +1,34 @@ +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' => '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)))); + + $filter = new ProjectActivityTaskIdFilter(1); + $filter->withQuery($query)->apply(); + $this->assertCount(1, $query->findAll()); + } +} diff --git a/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php b/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php new file mode 100644 index 00000000..6a7c23af --- /dev/null +++ b/tests/units/Filter/ProjectActivityTaskTitleFilterTest.php @@ -0,0 +1,34 @@ +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('test2'); + $filter->withQuery($query)->apply(); + $this->assertCount(1, $query->findAll()); + } +} diff --git a/tests/units/Helper/ProjectActivityHelperTest.php b/tests/units/Helper/ProjectActivityHelperTest.php new file mode 100644 index 00000000..88b2d352 --- /dev/null +++ b/tests/units/Helper/ProjectActivityHelperTest.php @@ -0,0 +1,97 @@ +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->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(3, $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->assertNotFalse($projectActivityModel->createEvent(1, 3, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(3)))); + + $helper = new ProjectActivityHelper($this->container); + $events = $helper->getProjectEvents(1); + + $this->assertCount(3, $events); + $this->assertEquals(3, $events[0]['task_id']); + $this->assertNotEmpty($events[0]['event_content']); + $this->assertNotEmpty($events[0]['event_title']); + $this->assertNotEmpty($events[0]['author']); + $this->assertInternalType('array', $events[0]['task']); + } + + public function testGetProjectsEvents() + { + $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(2, $projectModel->create(array('name' => 'P2'))); + $this->assertEquals(3, $projectModel->create(array('name' => 'P3'))); + + $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2))); + $this->assertEquals(3, $taskCreation->create(array('title' => 'Test', 'project_id' => 3))); + + $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)))); + $this->assertNotFalse($projectActivityModel->createEvent(3, 3, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(3)))); + + $helper = new ProjectActivityHelper($this->container); + $events = $helper->getProjectsEvents(array(1, 2)); + + $this->assertCount(2, $events); + $this->assertEquals(2, $events[0]['task_id']); + $this->assertNotEmpty($events[0]['event_content']); + $this->assertNotEmpty($events[0]['event_title']); + $this->assertNotEmpty($events[0]['author']); + $this->assertInternalType('array', $events[0]['task']); + } + + public function testGetTaskEvents() + { + $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->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)))); + + $helper = new ProjectActivityHelper($this->container); + $events = $helper->getTaskEvents(1); + + $this->assertCount(1, $events); + $this->assertEquals(1, $events[0]['task_id']); + $this->assertNotEmpty($events[0]['event_content']); + $this->assertNotEmpty($events[0]['event_title']); + $this->assertNotEmpty($events[0]['author']); + $this->assertInternalType('array', $events[0]['task']); + } +} diff --git a/tests/units/Model/ProjectActivityTest.php b/tests/units/Model/ProjectActivityTest.php index 27ea039d..a624cd86 100644 --- a/tests/units/Model/ProjectActivityTest.php +++ b/tests/units/Model/ProjectActivityTest.php @@ -10,90 +10,51 @@ use Kanboard\Model\Project; class ProjectActivityTest extends Base { - public function testDecode() - { - $e = new ProjectActivity($this->container); - $input = array('test'); - $serialized = serialize($input); - $json = json_encode($input); - - $this->assertEquals($input, $e->decode($serialized)); - $this->assertEquals($input, $e->decode($json)); - } - public function testCreation() { - $e = new ProjectActivity($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFinder($this->container); - $p = new Project($this->container); + $projectActivity = new ProjectActivity($this->container); + $taskCreation = new TaskCreation($this->container); + $taskFinder = new TaskFinder($this->container); + $projectModel = new Project($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1))); - $this->assertEquals(2, $tc->create(array('title' => 'Task #2', 'project_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertEquals(2, $taskCreation->create(array('title' => 'Task #2', 'project_id' => 1))); - $this->assertTrue($e->createEvent(1, 1, 1, Task::EVENT_CLOSE, array('task' => $tf->getbyId(1)))); - $this->assertTrue($e->createEvent(1, 2, 1, Task::EVENT_UPDATE, array('task' => $tf->getById(2)))); - $this->assertFalse($e->createEvent(1, 1, 0, Task::EVENT_OPEN, array('task' => $tf->getbyId(1)))); + $this->assertTrue($projectActivity->createEvent(1, 1, 1, Task::EVENT_CLOSE, array('task' => $taskFinder->getbyId(1)))); + $this->assertTrue($projectActivity->createEvent(1, 2, 1, Task::EVENT_UPDATE, array('task' => $taskFinder->getById(2)))); + $this->assertFalse($projectActivity->createEvent(1, 1, 0, Task::EVENT_OPEN, array('task' => $taskFinder->getbyId(1)))); - $events = $e->getProject(1); + $events = $projectActivity->getQuery()->desc('id')->findAll(); - $this->assertNotEmpty($events); - $this->assertTrue(is_array($events)); - $this->assertEquals(2, count($events)); + $this->assertCount(2, $events); $this->assertEquals(time(), $events[0]['date_creation'], '', 1); $this->assertEquals(Task::EVENT_UPDATE, $events[0]['event_name']); $this->assertEquals(Task::EVENT_CLOSE, $events[1]['event_name']); } - public function testFetchAllContent() - { - $e = new ProjectActivity($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFinder($this->container); - $p = new Project($this->container); - - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1))); - - $nb_events = 80; - - for ($i = 0; $i < $nb_events; $i++) { - $this->assertTrue($e->createEvent(1, 1, 1, Task::EVENT_UPDATE, array('task' => $tf->getbyId(1)))); - } - - $events = $e->getProject(1); - - $this->assertNotEmpty($events); - $this->assertTrue(is_array($events)); - $this->assertEquals(50, count($events)); - $this->assertEquals('admin', $events[0]['author']); - $this->assertNotEmpty($events[0]['event_title']); - $this->assertNotEmpty($events[0]['event_content']); - } - public function testCleanup() { - $e = new ProjectActivity($this->container); - $tc = new TaskCreation($this->container); - $tf = new TaskFinder($this->container); - $p = new Project($this->container); + $projectActivity = new ProjectActivity($this->container); + $taskCreation = new TaskCreation($this->container); + $taskFinder = new TaskFinder($this->container); + $projectModel = new Project($this->container); - $this->assertEquals(1, $p->create(array('name' => 'Project #1'))); - $this->assertEquals(1, $tc->create(array('title' => 'Task #1', 'project_id' => 1))); + $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1'))); + $this->assertEquals(1, $taskCreation->create(array('title' => 'Task #1', 'project_id' => 1))); $max = 15; $nb_events = 100; - $task = $tf->getbyId(1); + $task = $taskFinder->getbyId(1); for ($i = 0; $i < $nb_events; $i++) { - $this->assertTrue($e->createEvent(1, 1, 1, Task::EVENT_CLOSE, array('task' => $task))); + $this->assertTrue($projectActivity->createEvent(1, 1, 1, Task::EVENT_CLOSE, array('task' => $task))); } $this->assertEquals($nb_events, $this->container['db']->table('project_activities')->count()); - $e->cleanup($max); + $projectActivity->cleanup($max); - $events = $e->getProject(1); + $events = $projectActivity->getQuery()->desc('id')->findAll(); $this->assertNotEmpty($events); $this->assertCount($max, $events); -- 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 'tests/units') 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