From 11858be4e8d5aba983700c6cba1c4d0a33ea8e9d Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 9 Apr 2016 22:42:17 -0400 Subject: Filter refactoring --- app/ServiceProvider/FilterProvider.php | 112 +++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 app/ServiceProvider/FilterProvider.php (limited to 'app/ServiceProvider/FilterProvider.php') 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; + } +} -- 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 'app/ServiceProvider/FilterProvider.php') 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 'app/ServiceProvider/FilterProvider.php') 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 'app/ServiceProvider/FilterProvider.php') 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 'app/ServiceProvider/FilterProvider.php') 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 @@ +
+ + +
+ +
+ + +
+

+

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