summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--ChangeLog7
-rw-r--r--app/Controller/Analytic.php7
-rw-r--r--app/Controller/Board.php10
-rw-r--r--app/Controller/Calendar.php39
-rw-r--r--app/Controller/Gantt.php25
-rw-r--r--app/Controller/GroupHelper.php8
-rw-r--r--app/Controller/Ical.php59
-rw-r--r--app/Controller/Listing.php8
-rw-r--r--app/Controller/Search.php14
-rw-r--r--app/Controller/TaskHelper.php38
-rw-r--r--app/Controller/UserHelper.php13
-rw-r--r--app/Core/Action/ActionManager.php2
-rw-r--r--app/Core/Base.php30
-rw-r--r--app/Core/ExternalLink/ExternalLinkManager.php2
-rw-r--r--app/Core/Filter/CriteriaInterface.php40
-rw-r--r--app/Core/Filter/FilterInterface.php56
-rw-r--r--app/Core/Filter/FormatterInterface.php31
-rw-r--r--app/Core/Filter/Lexer.php153
-rw-r--r--app/Core/Filter/LexerBuilder.php151
-rw-r--r--app/Core/Filter/OrCriteria.php68
-rw-r--r--app/Core/Filter/QueryBuilder.php103
-rw-r--r--app/Core/Helper.php2
-rw-r--r--app/Core/Http/Response.php14
-rw-r--r--app/Core/Lexer.php161
-rw-r--r--app/Filter/BaseFilter.php119
-rw-r--r--app/Filter/ProjectGroupRoleProjectFilter.php38
-rw-r--r--app/Filter/ProjectGroupRoleUsernameFilter.php44
-rw-r--r--app/Filter/ProjectIdsFilter.php43
-rw-r--r--app/Filter/ProjectStatusFilter.php45
-rw-r--r--app/Filter/ProjectTypeFilter.php45
-rw-r--r--app/Filter/ProjectUserRoleProjectFilter.php38
-rw-r--r--app/Filter/ProjectUserRoleUsernameFilter.php41
-rw-r--r--app/Filter/TaskAssigneeFilter.php75
-rw-r--r--app/Filter/TaskCategoryFilter.php46
-rw-r--r--app/Filter/TaskColorFilter.php60
-rw-r--r--app/Filter/TaskColumnFilter.php44
-rw-r--r--app/Filter/TaskCompletionDateFilter.php38
-rw-r--r--app/Filter/TaskCreationDateFilter.php38
-rw-r--r--app/Filter/TaskDescriptionFilter.php38
-rw-r--r--app/Filter/TaskDueDateFilter.php41
-rw-r--r--app/Filter/TaskDueDateRangeFilter.php39
-rw-r--r--app/Filter/TaskIdExclusionFilter.php38
-rw-r--r--app/Filter/TaskIdFilter.php38
-rw-r--r--app/Filter/TaskLinkFilter.php85
-rw-r--r--app/Filter/TaskModificationDateFilter.php38
-rw-r--r--app/Filter/TaskProjectFilter.php44
-rw-r--r--app/Filter/TaskProjectsFilter.php38
-rw-r--r--app/Filter/TaskReferenceFilter.php38
-rw-r--r--app/Filter/TaskStartDateFilter.php38
-rw-r--r--app/Filter/TaskStatusFilter.php43
-rw-r--r--app/Filter/TaskSubtaskAssigneeFilter.php140
-rw-r--r--app/Filter/TaskSwimlaneFilter.php50
-rw-r--r--app/Filter/TaskTitleFilter.php46
-rw-r--r--app/Filter/UserNameFilter.php35
-rw-r--r--app/Formatter/BaseFormatter.php37
-rw-r--r--app/Formatter/BaseTaskCalendarFormatter.php (renamed from app/Formatter/TaskFilterCalendarEvent.php)37
-rw-r--r--app/Formatter/BoardFormatter.php56
-rw-r--r--app/Formatter/FormatterInterface.php14
-rw-r--r--app/Formatter/GroupAutoCompleteFormatter.php28
-rw-r--r--app/Formatter/ProjectGanttFormatter.php39
-rw-r--r--app/Formatter/SubtaskTimeTrackingCalendarFormatter.php38
-rw-r--r--app/Formatter/TaskAutoCompleteFormatter.php (renamed from app/Formatter/TaskFilterAutoCompleteFormatter.php)12
-rw-r--r--app/Formatter/TaskCalendarFormatter.php (renamed from app/Formatter/TaskFilterCalendarFormatter.php)30
-rw-r--r--app/Formatter/TaskGanttFormatter.php (renamed from app/Formatter/TaskFilterGanttFormatter.php)14
-rw-r--r--app/Formatter/TaskICalFormatter.php (renamed from app/Formatter/TaskFilterICalendarFormatter.php)17
-rw-r--r--app/Formatter/UserAutoCompleteFormatter.php (renamed from app/Formatter/UserFilterAutoCompleteFormatter.php)6
-rw-r--r--app/Helper/CalendarHelper.php112
-rw-r--r--app/Helper/ICalHelper.php38
-rw-r--r--app/Model/AvatarFile.php1
-rw-r--r--app/Model/Base.php24
-rw-r--r--app/Model/Project.php14
-rw-r--r--app/Model/ProjectActivity.php6
-rw-r--r--app/Model/ProjectGroupRoleFilter.php89
-rw-r--r--app/Model/ProjectPermission.php18
-rw-r--r--app/Model/ProjectUserRole.php4
-rw-r--r--app/Model/ProjectUserRoleFilter.php88
-rw-r--r--app/Model/Setting.php1
-rw-r--r--app/Model/SubtaskTimeTracking.php88
-rw-r--r--app/Model/TaskFilter.php745
-rw-r--r--app/Model/TaskFinder.php21
-rw-r--r--app/Model/UserFilter.php80
-rw-r--r--app/ServiceProvider/ClassProvider.php12
-rw-r--r--app/ServiceProvider/FilterProvider.php112
-rw-r--r--app/ServiceProvider/HelperProvider.php2
-rw-r--r--app/common.php1
-rw-r--r--composer.lock20
-rw-r--r--doc/installation.markdown2
-rw-r--r--doc/plugin-hooks.markdown9
-rw-r--r--doc/update.markdown2
-rw-r--r--tests/units/Base.php1
-rw-r--r--tests/units/Core/Filter/LexerBuilderTest.php106
-rw-r--r--tests/units/Core/Filter/LexerTest.php100
-rw-r--r--tests/units/Core/Filter/OrCriteriaTest.php58
-rw-r--r--tests/units/Core/LexerTest.php468
-rw-r--r--tests/units/Filter/TaskAssigneeFilterTest.php159
-rw-r--r--tests/units/Formatter/TaskFilterCalendarFormatterTest.php21
-rw-r--r--tests/units/Formatter/TaskFilterGanttFormatterTest.php24
-rw-r--r--tests/units/Formatter/TaskFilterICalendarFormatterTest.php74
-rw-r--r--tests/units/Model/SubtaskTimeTrackingTest.php77
-rw-r--r--tests/units/Model/TaskFilterTest.php624
101 files changed, 3234 insertions, 2840 deletions
diff --git a/.travis.yml b/.travis.yml
index 1c132a0b..40af3ca8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -23,7 +23,6 @@ before_script:
- phpenv config-rm xdebug.ini
- phpenv config-add tests/php.ini
- composer install
- - php -i
script:
- phpunit -c tests/units.$DB.xml
diff --git a/ChangeLog b/ChangeLog
index f07ba9e8..ea12d9b9 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+Version 1.0.28 (unreleased)
+--------------
+
+Improvements:
+
+* Filter/Lexer/QueryBuilder refactoring
+
Version 1.0.27
--------------
diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php
index 6b0730b0..35bc3048 100644
--- a/app/Controller/Analytic.php
+++ b/app/Controller/Analytic.php
@@ -2,6 +2,7 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -44,13 +45,15 @@ class Analytic extends Base
public function compareHours()
{
$project = $this->getProject();
- $query = $this->taskFilter->create()->filterByProject($project['id'])->getQuery();
$paginator = $this->paginator
->setUrl('analytic', 'compareHours', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
- ->setQuery($query)
+ ->setQuery($this->taskQuery
+ ->withFilter(new TaskProjectFilter($project['id']))
+ ->getQuery()
+ )
->calculate();
$this->response->html($this->helper->layout->analytic('analytic/compare_hours', array(
diff --git a/app/Controller/Board.php b/app/Controller/Board.php
index 51344bd3..67e99b81 100644
--- a/app/Controller/Board.php
+++ b/app/Controller/Board.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Formatter\BoardFormatter;
+
/**
* Board controller
*
@@ -51,12 +53,14 @@ class Board extends Base
$search = $this->helper->projectHeader->getSearchQuery($project);
$this->response->html($this->helper->layout->app('board/view_private', array(
- 'swimlanes' => $this->taskFilter->search($search)->getBoard($project['id']),
'project' => $project,
'title' => $project['name'],
'description' => $this->helper->projectHeader->getDescription($project),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
+ 'swimlanes' => $this->taskLexer
+ ->build($search)
+ ->format(BoardFormatter::getInstance($this->container)->setProjectId($project['id']))
)));
}
@@ -178,9 +182,11 @@ class Board extends Base
{
return $this->template->render('board/table_container', array(
'project' => $this->project->getById($project_id),
- 'swimlanes' => $this->taskFilter->search($this->userSession->getFilters($project_id))->getBoard($project_id),
'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'),
'board_highlight_period' => $this->config->get('board_highlight_period'),
+ 'swimlanes' => $this->taskLexer
+ ->build($this->userSession->getFilters($project_id))
+ ->format(BoardFormatter::getInstance($this->container)->setProjectId($project_id))
));
}
}
diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php
index af31ae47..2517286d 100644
--- a/app/Controller/Calendar.php
+++ b/app/Controller/Calendar.php
@@ -2,6 +2,9 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskStatusFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -40,21 +43,11 @@ class Calendar extends Base
$project_id = $this->request->getIntegerParam('project_id');
$start = $this->request->getStringParam('start');
$end = $this->request->getStringParam('end');
+ $search = $this->userSession->getFilters($project_id);
+ $queryBuilder = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project_id));
- // Common filter
- $filter = $this->taskFilterCalendarFormatter
- ->search($this->userSession->getFilters($project_id))
- ->filterByProject($project_id);
-
- // Tasks
- if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
- $events = $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format();
- } else {
- $events = $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format();
- }
-
- // Tasks with due date
- $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format());
+ $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
+ $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
$events = $this->hook->merge('controller:calendar:project:events', $events, array(
'project_id' => $project_id,
@@ -75,21 +68,15 @@ class Calendar extends Base
$user_id = $this->request->getIntegerParam('user_id');
$start = $this->request->getStringParam('start');
$end = $this->request->getStringParam('end');
- $filter = $this->taskFilterCalendarFormatter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN);
+ $queryBuilder = $this->taskQuery
+ ->withFilter(new TaskAssigneeFilter($user_id))
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN));
- // Task with due date
- $events = $filter->copy()->filterByDueDateRange($start, $end)->setColumns('date_due')->setFullDay()->format();
-
- // Tasks
- if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') {
- $events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->setColumns('date_creation', 'date_completed')->format());
- } else {
- $events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->setColumns('date_started', 'date_completed')->format());
- }
+ $events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
+ $events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
- // Subtasks time tracking
if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) {
- $events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end));
+ $events = array_merge($events, $this->helper->calendar->getSubtaskTimeTrackingEvents($user_id, $start, $end));
}
$events = $this->hook->merge('controller:calendar:user:events', $events, array(
diff --git a/app/Controller/Gantt.php b/app/Controller/Gantt.php
index 02ee946c..5e9ad55e 100644
--- a/app/Controller/Gantt.php
+++ b/app/Controller/Gantt.php
@@ -2,7 +2,14 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\ProjectIdsFilter;
+use Kanboard\Filter\ProjectStatusFilter;
+use Kanboard\Filter\ProjectTypeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Formatter\ProjectGanttFormatter;
+use Kanboard\Formatter\TaskGanttFormatter;
use Kanboard\Model\Task as TaskModel;
+use Kanboard\Model\Project as ProjectModel;
/**
* Gantt controller
@@ -17,14 +24,16 @@ class Gantt extends Base
*/
public function projects()
{
- if ($this->userSession->isAdmin()) {
- $project_ids = $this->project->getAllIds();
- } else {
- $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
- }
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $filter = $this->projectQuery
+ ->withFilter(new ProjectTypeFilter(ProjectModel::TYPE_TEAM))
+ ->withFilter(new ProjectStatusFilter(ProjectModel::ACTIVE))
+ ->withFilter(new ProjectIdsFilter($project_ids));
+
+ $filter->getQuery()->asc(ProjectModel::TABLE.'.start_date');
$this->response->html($this->helper->layout->app('gantt/projects', array(
- 'projects' => $this->projectGanttFormatter->filter($project_ids)->format(),
+ 'projects' => $filter->format(new ProjectGanttFormatter($this->container)),
'title' => t('Gantt chart for all projects'),
)));
}
@@ -56,8 +65,8 @@ class Gantt extends Base
{
$project = $this->getProject();
$search = $this->helper->projectHeader->getSearchQuery($project);
- $filter = $this->taskFilterGanttFormatter->search($search)->filterByProject($project['id']);
$sorting = $this->request->getStringParam('sorting', 'board');
+ $filter = $this->taskLexer->build($search)->withFilter(new TaskProjectFilter($project['id']));
if ($sorting === 'date') {
$filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation');
@@ -70,7 +79,7 @@ class Gantt extends Base
'title' => $project['name'],
'description' => $this->helper->projectHeader->getDescription($project),
'sorting' => $sorting,
- 'tasks' => $filter->format(),
+ 'tasks' => $filter->format(new TaskGanttFormatter($this->container)),
)));
}
diff --git a/app/Controller/GroupHelper.php b/app/Controller/GroupHelper.php
index 34f522a6..429614c2 100644
--- a/app/Controller/GroupHelper.php
+++ b/app/Controller/GroupHelper.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Formatter\GroupAutoCompleteFormatter;
+
/**
* Group Helper
*
@@ -11,14 +13,14 @@ namespace Kanboard\Controller;
class GroupHelper extends Base
{
/**
- * Group autocompletion (Ajax)
+ * Group auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $groups = $this->groupManager->find($search);
- $this->response->json($this->groupAutoCompleteFormatter->setGroups($groups)->format());
+ $formatter = new GroupAutoCompleteFormatter($this->groupManager->find($search));
+ $this->response->json($formatter->format());
}
}
diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php
index f1ea6d8f..8fe97b46 100644
--- a/app/Controller/Ical.php
+++ b/app/Controller/Ical.php
@@ -2,7 +2,11 @@
namespace Kanboard\Controller;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskStatusFilter;
+use Kanboard\Formatter\TaskICalFormatter;
use Kanboard\Model\Task as TaskModel;
use Eluceo\iCal\Component\Calendar as iCalendar;
@@ -30,10 +34,11 @@ class Ical extends Base
}
// Common filter
- $filter = $this->taskFilterICalendarFormatter
- ->create()
- ->filterByStatus(TaskModel::STATUS_OPEN)
- ->filterByOwner($user['id']);
+ $queryBuilder = new QueryBuilder();
+ $queryBuilder
+ ->withQuery($this->taskFinder->getICalQuery())
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
+ ->withFilter(new TaskAssigneeFilter($user['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard');
@@ -41,7 +46,7 @@ class Ical extends Base
$calendar->setDescription($user['name'] ?: $user['username']);
$calendar->setPublishedTTL('PT1H');
- $this->renderCalendar($filter, $calendar);
+ $this->renderCalendar($queryBuilder, $calendar);
}
/**
@@ -60,10 +65,11 @@ class Ical extends Base
}
// Common filter
- $filter = $this->taskFilterICalendarFormatter
- ->create()
- ->filterByStatus(TaskModel::STATUS_OPEN)
- ->filterByProject($project['id']);
+ $queryBuilder = new QueryBuilder();
+ $queryBuilder
+ ->withQuery($this->taskFinder->getICalQuery())
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
+ ->withFilter(new TaskProjectFilter($project['id']));
// Calendar properties
$calendar = new iCalendar('Kanboard');
@@ -71,7 +77,7 @@ class Ical extends Base
$calendar->setDescription($project['name']);
$calendar->setPublishedTTL('PT1H');
- $this->renderCalendar($filter, $calendar);
+ $this->renderCalendar($queryBuilder, $calendar);
}
/**
@@ -79,37 +85,14 @@ class Ical extends Base
*
* @access private
*/
- private function renderCalendar(TaskFilter $filter, iCalendar $calendar)
+ private function renderCalendar(QueryBuilder $queryBuilder, iCalendar $calendar)
{
$start = $this->request->getStringParam('start', strtotime('-2 month'));
$end = $this->request->getStringParam('end', strtotime('+6 months'));
- // Tasks
- if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') {
- $filter
- ->copy()
- ->filterByCreationDateRange($start, $end)
- ->setColumns('date_creation', 'date_completed')
- ->setCalendar($calendar)
- ->addDateTimeEvents();
- } else {
- $filter
- ->copy()
- ->filterByStartDateRange($start, $end)
- ->setColumns('date_started', 'date_completed')
- ->setCalendar($calendar)
- ->addDateTimeEvents($calendar);
- }
-
- // Tasks with due date
- $filter
- ->copy()
- ->filterByDueDateRange($start, $end)
- ->setColumns('date_due')
- ->setCalendar($calendar)
- ->addFullDayEvents($calendar);
+ $this->helper->ical->addTaskDateDueEvents($queryBuilder, $calendar, $start, $end);
- $this->response->contentType('text/calendar; charset=utf-8');
- echo $filter->setCalendar($calendar)->format();
+ $formatter = new TaskICalFormatter($this->container);
+ $this->response->ical($formatter->setCalendar($calendar)->format());
}
}
diff --git a/app/Controller/Listing.php b/app/Controller/Listing.php
index 9931c346..2024ff03 100644
--- a/app/Controller/Listing.php
+++ b/app/Controller/Listing.php
@@ -2,6 +2,7 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectFilter;
use Kanboard\Model\Task as TaskModel;
/**
@@ -21,14 +22,17 @@ class Listing extends Base
{
$project = $this->getProject();
$search = $this->helper->projectHeader->getSearchQuery($project);
- $query = $this->taskFilter->search($search)->filterByProject($project['id'])->getQuery();
$paginator = $this->paginator
->setUrl('listing', 'show', array('project_id' => $project['id']))
->setMax(30)
->setOrder(TaskModel::TABLE.'.id')
->setDirection('DESC')
- ->setQuery($query)
+ ->setQuery($this->taskLexer
+ ->build($search)
+ ->withFilter(new TaskProjectFilter($project['id']))
+ ->getQuery()
+ )
->calculate();
$this->response->html($this->helper->layout->app('listing/show', array(
diff --git a/app/Controller/Search.php b/app/Controller/Search.php
index 9b9b9e65..840a90c8 100644
--- a/app/Controller/Search.php
+++ b/app/Controller/Search.php
@@ -2,6 +2,8 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskProjectsFilter;
+
/**
* Search controller
*
@@ -23,14 +25,12 @@ class Search extends Base
->setDirection('DESC');
if ($search !== '' && ! empty($projects)) {
- $query = $this
- ->taskFilter
- ->search($search)
- ->filterByProjects(array_keys($projects))
- ->getQuery();
-
$paginator
- ->setQuery($query)
+ ->setQuery($this->taskLexer
+ ->build($search)
+ ->withFilter(new TaskProjectsFilter(array_keys($projects)))
+ ->getQuery()
+ )
->calculate();
$nb_tasks = $paginator->getTotal();
diff --git a/app/Controller/TaskHelper.php b/app/Controller/TaskHelper.php
index 7e340a6a..6835ab2b 100644
--- a/app/Controller/TaskHelper.php
+++ b/app/Controller/TaskHelper.php
@@ -2,6 +2,12 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\TaskIdExclusionFilter;
+use Kanboard\Filter\TaskIdFilter;
+use Kanboard\Filter\TaskProjectsFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Formatter\TaskAutoCompleteFormatter;
+
/**
* Task Ajax Helper
*
@@ -11,31 +17,33 @@ namespace Kanboard\Controller;
class TaskHelper extends Base
{
/**
- * Task autocompletion (Ajax)
+ * Task auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $projects = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $project_ids = $this->projectPermission->getActiveProjectIds($this->userSession->getId());
+ $exclude_task_id = $this->request->getIntegerParam('exclude_task_id');
- if (empty($projects)) {
+ if (empty($project_ids)) {
$this->response->json(array());
- }
+ } else {
- $filter = $this->taskFilterAutoCompleteFormatter
- ->create()
- ->filterByProjects($projects)
- ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
+ $filter = $this->taskQuery->withFilter(new TaskProjectsFilter($project_ids));
- // Search by task id or by title
- if (ctype_digit($search)) {
- $filter->filterById($search);
- } else {
- $filter->filterByTitle($search);
- }
+ if (! empty($exclude_task_id)) {
+ $filter->withFilter(new TaskIdExclusionFilter(array($exclude_task_id)));
+ }
+
+ if (ctype_digit($search)) {
+ $filter->withFilter(new TaskIdFilter($search));
+ } else {
+ $filter->withFilter(new TaskTitleFilter($search));
+ }
- $this->response->json($filter->format());
+ $this->response->json($filter->format(new TaskAutoCompleteFormatter($this->container)));
+ }
}
}
diff --git a/app/Controller/UserHelper.php b/app/Controller/UserHelper.php
index 041ed2c8..47bbe554 100644
--- a/app/Controller/UserHelper.php
+++ b/app/Controller/UserHelper.php
@@ -2,6 +2,10 @@
namespace Kanboard\Controller;
+use Kanboard\Filter\UserNameFilter;
+use Kanboard\Formatter\UserAutoCompleteFormatter;
+use Kanboard\Model\User as UserModel;
+
/**
* User Helper
*
@@ -11,19 +15,20 @@ namespace Kanboard\Controller;
class UserHelper extends Base
{
/**
- * User autocompletion (Ajax)
+ * User auto-completion (Ajax)
*
* @access public
*/
public function autocomplete()
{
$search = $this->request->getStringParam('term');
- $users = $this->userFilterAutoCompleteFormatter->create($search)->filterByUsernameOrByName()->format();
- $this->response->json($users);
+ $filter = $this->userQuery->withFilter(new UserNameFilter($search));
+ $filter->getQuery()->asc(UserModel::TABLE.'.name')->asc(UserModel::TABLE.'.username');
+ $this->response->json($filter->format(new UserAutoCompleteFormatter($this->container)));
}
/**
- * User mention autocompletion (Ajax)
+ * User mention auto-completion (Ajax)
*
* @access public
*/
diff --git a/app/Core/Action/ActionManager.php b/app/Core/Action/ActionManager.php
index f1ea8abe..dfa5a140 100644
--- a/app/Core/Action/ActionManager.php
+++ b/app/Core/Action/ActionManager.php
@@ -18,7 +18,7 @@ class ActionManager extends Base
* List of automatic actions
*
* @access private
- * @var array
+ * @var ActionBase[]
*/
private $actions = array();
diff --git a/app/Core/Base.php b/app/Core/Base.php
index 74573e94..8c6b7620 100644
--- a/app/Core/Base.php
+++ b/app/Core/Base.php
@@ -48,16 +48,8 @@ use Pimple\Container;
* @property \Kanboard\Core\User\UserSession $userSession
* @property \Kanboard\Core\DateParser $dateParser
* @property \Kanboard\Core\Helper $helper
- * @property \Kanboard\Core\Lexer $lexer
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Template $template
- * @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter
- * @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter
- * @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter
- * @property \Kanboard\Formatter\TaskFilterCalendarFormatter $taskFilterCalendarFormatter
- * @property \Kanboard\Formatter\TaskFilterICalendarFormatter $taskFilterICalendarFormatter
- * @property \Kanboard\Formatter\UserFilterAutoCompleteFormatter $userFilterAutoCompleteFormatter
- * @property \Kanboard\Formatter\GroupAutoCompleteFormatter $groupAutoCompleteFormatter
* @property \Kanboard\Model\Action $action
* @property \Kanboard\Model\ActionParameter $actionParameter
* @property \Kanboard\Model\AvatarFile $avatarFile
@@ -85,7 +77,6 @@ use Pimple\Container;
* @property \Kanboard\Model\ProjectMetadata $projectMetadata
* @property \Kanboard\Model\ProjectPermission $projectPermission
* @property \Kanboard\Model\ProjectUserRole $projectUserRole
- * @property \Kanboard\Model\projectUserRoleFilter $projectUserRoleFilter
* @property \Kanboard\Model\ProjectGroupRole $projectGroupRole
* @property \Kanboard\Model\ProjectNotification $projectNotification
* @property \Kanboard\Model\ProjectNotificationType $projectNotificationType
@@ -99,7 +90,6 @@ use Pimple\Container;
* @property \Kanboard\Model\TaskDuplication $taskDuplication
* @property \Kanboard\Model\TaskExternalLink $taskExternalLink
* @property \Kanboard\Model\TaskFinder $taskFinder
- * @property \Kanboard\Model\TaskFilter $taskFilter
* @property \Kanboard\Model\TaskLink $taskLink
* @property \Kanboard\Model\TaskModification $taskModification
* @property \Kanboard\Model\TaskPermission $taskPermission
@@ -137,6 +127,12 @@ use Pimple\Container;
* @property \Kanboard\Export\SubtaskExport $subtaskExport
* @property \Kanboard\Export\TaskExport $taskExport
* @property \Kanboard\Export\TransitionExport $transitionExport
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectGroupRoleQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectUserRoleQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $userQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $projectQuery
+ * @property \Kanboard\Core\Filter\QueryBuilder $taskQuery
+ * @property \Kanboard\Core\Filter\LexerBuilder $taskLexer
* @property \Psr\Log\LoggerInterface $logger
* @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
@@ -173,4 +169,18 @@ abstract class Base
{
return $this->container[$name];
}
+
+ /**
+ * Get object instance
+ *
+ * @static
+ * @access public
+ * @param Container $container
+ * @return static
+ */
+ public static function getInstance(Container $container)
+ {
+ $self = new static($container);
+ return $self;
+ }
}
diff --git a/app/Core/ExternalLink/ExternalLinkManager.php b/app/Core/ExternalLink/ExternalLinkManager.php
index 1fa423c2..804e6b34 100644
--- a/app/Core/ExternalLink/ExternalLinkManager.php
+++ b/app/Core/ExternalLink/ExternalLinkManager.php
@@ -23,7 +23,7 @@ class ExternalLinkManager extends Base
* Registered providers
*
* @access private
- * @var array
+ * @var ExternalLinkProviderInterface[]
*/
private $providers = array();
diff --git a/app/Core/Filter/CriteriaInterface.php b/app/Core/Filter/CriteriaInterface.php
new file mode 100644
index 00000000..009c4bd3
--- /dev/null
+++ b/app/Core/Filter/CriteriaInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Criteria Interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface CriteriaInterface
+{
+ /**
+ * Set the Query
+ *
+ * @access public
+ * @param Table $query
+ * @return CriteriaInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Set filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return CriteriaInterface
+ */
+ public function withFilter(FilterInterface $filter);
+
+ /**
+ * Apply condition
+ *
+ * @access public
+ * @return CriteriaInterface
+ */
+ public function apply();
+}
diff --git a/app/Core/Filter/FilterInterface.php b/app/Core/Filter/FilterInterface.php
new file mode 100644
index 00000000..7b66ec28
--- /dev/null
+++ b/app/Core/Filter/FilterInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Filter Interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface FilterInterface
+{
+ /**
+ * BaseFilter constructor
+ *
+ * @access public
+ * @param mixed $value
+ */
+ public function __construct($value = null);
+
+ /**
+ * Set the value
+ *
+ * @access public
+ * @param string $value
+ * @return FilterInterface
+ */
+ public function withValue($value);
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FilterInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes();
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply();
+}
diff --git a/app/Core/Filter/FormatterInterface.php b/app/Core/Filter/FormatterInterface.php
new file mode 100644
index 00000000..b7c04c51
--- /dev/null
+++ b/app/Core/Filter/FormatterInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Formatter interface
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+interface FormatterInterface
+{
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query);
+
+ /**
+ * Apply formatter
+ *
+ * @access public
+ * @return mixed
+ */
+ public function format();
+}
diff --git a/app/Core/Filter/Lexer.php b/app/Core/Filter/Lexer.php
new file mode 100644
index 00000000..041b58d3
--- /dev/null
+++ b/app/Core/Filter/Lexer.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+/**
+ * Lexer
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class Lexer
+{
+ /**
+ * Current position
+ *
+ * @access private
+ * @var integer
+ */
+ private $offset = 0;
+
+ /**
+ * Token map
+ *
+ * @access private
+ * @var array
+ */
+ private $tokenMap = array(
+ "/^(\s+)/" => 'T_WHITESPACE',
+ '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
+ '/^(yesterday|tomorrow|today)/' => 'T_DATE',
+ '/^("(.*?)")/' => 'T_STRING',
+ "/^(\w+)/" => 'T_STRING',
+ "/^(#\d+)/" => 'T_STRING',
+ );
+
+ /**
+ * Default token
+ *
+ * @access private
+ * @var string
+ */
+ private $defaultToken = '';
+
+ /**
+ * Add token
+ *
+ * @access public
+ * @param string $regex
+ * @param string $token
+ * @return $this
+ */
+ public function addToken($regex, $token)
+ {
+ $this->tokenMap = array($regex => $token) + $this->tokenMap;
+ return $this;
+ }
+
+ /**
+ * Set default token
+ *
+ * @access public
+ * @param string $token
+ * @return $this
+ */
+ public function setDefaultToken($token)
+ {
+ $this->defaultToken = $token;
+ return $this;
+ }
+
+ /**
+ * Tokenize input string
+ *
+ * @access public
+ * @param string $input
+ * @return array
+ */
+ public function tokenize($input)
+ {
+ $tokens = array();
+ $this->offset = 0;
+
+ while (isset($input[$this->offset])) {
+ $result = $this->match(substr($input, $this->offset));
+
+ if ($result === false) {
+ return array();
+ }
+
+ $tokens[] = $result;
+ }
+
+ return $this->map($tokens);
+ }
+
+ /**
+ * Find a token that match and move the offset
+ *
+ * @access protected
+ * @param string $string
+ * @return array|boolean
+ */
+ protected function match($string)
+ {
+ foreach ($this->tokenMap as $pattern => $name) {
+ if (preg_match($pattern, $string, $matches)) {
+ $this->offset += strlen($matches[1]);
+
+ return array(
+ 'match' => trim($matches[1], '"'),
+ 'token' => $name,
+ );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Build map of tokens and matches
+ *
+ * @access protected
+ * @param array $tokens
+ * @return array
+ */
+ protected function map(array $tokens)
+ {
+ $map = array();
+ $leftOver = '';
+
+ while (false !== ($token = current($tokens))) {
+ if ($token['token'] === 'T_STRING' || $token['token'] === 'T_WHITESPACE') {
+ $leftOver .= $token['match'];
+ } else {
+ $next = next($tokens);
+
+ if ($next !== false && in_array($next['token'], array('T_STRING', 'T_DATE'))) {
+ $map[$token['token']][] = $next['match'];
+ }
+ }
+
+ next($tokens);
+ }
+
+ $leftOver = trim($leftOver);
+
+ if ($this->defaultToken !== '' && $leftOver !== '') {
+ $map[$this->defaultToken] = array($leftOver);
+ }
+
+ return $map;
+ }
+}
diff --git a/app/Core/Filter/LexerBuilder.php b/app/Core/Filter/LexerBuilder.php
new file mode 100644
index 00000000..7a9a714f
--- /dev/null
+++ b/app/Core/Filter/LexerBuilder.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Lexer Builder
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class LexerBuilder
+{
+ /**
+ * Lexer object
+ *
+ * @access protected
+ * @var Lexer
+ */
+ protected $lexer;
+
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * List of filters
+ *
+ * @access protected
+ * @var FilterInterface[]
+ */
+ protected $filters;
+
+ /**
+ * QueryBuilder object
+ *
+ * @access protected
+ * @var QueryBuilder
+ */
+ protected $queryBuilder;
+
+ /**
+ * Constructor
+ *
+ * @access public
+ */
+ public function __construct()
+ {
+ $this->lexer = new Lexer;
+ $this->queryBuilder = new QueryBuilder();
+ }
+
+ /**
+ * Add a filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @param bool $default
+ * @return LexerBuilder
+ */
+ public function withFilter(FilterInterface $filter, $default = false)
+ {
+ $attributes = $filter->getAttributes();
+
+ foreach ($attributes as $attribute) {
+ $this->filters[$attribute] = $filter;
+ $this->lexer->addToken(sprintf("/^(%s:)/", $attribute), $attribute);
+
+ if ($default) {
+ $this->lexer->setDefaultToken($attribute);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the query
+ *
+ * @access public
+ * @param Table $query
+ * @return LexerBuilder
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ $this->queryBuilder->withQuery($this->query);
+ return $this;
+ }
+
+ /**
+ * Parse the input and build the query
+ *
+ * @access public
+ * @param string $input
+ * @return QueryBuilder
+ */
+ public function build($input)
+ {
+ $tokens = $this->lexer->tokenize($input);
+
+ foreach ($tokens as $token => $values) {
+ if (isset($this->filters[$token])) {
+ $this->applyFilters($this->filters[$token], $values);
+ }
+ }
+
+ return $this->queryBuilder;
+ }
+
+ /**
+ * Apply filters to the query
+ *
+ * @access protected
+ * @param FilterInterface $filter
+ * @param array $values
+ */
+ protected function applyFilters(FilterInterface $filter, array $values)
+ {
+ $len = count($values);
+
+ if ($len > 1) {
+ $criteria = new OrCriteria();
+ $criteria->withQuery($this->query);
+
+ foreach ($values as $value) {
+ $currentFilter = clone($filter);
+ $criteria->withFilter($currentFilter->withValue($value));
+ }
+
+ $this->queryBuilder->withCriteria($criteria);
+ } elseif ($len === 1) {
+ $this->queryBuilder->withFilter($filter->withValue($values[0]));
+ }
+ }
+
+ /**
+ * Clone object with deep copy
+ */
+ public function __clone()
+ {
+ $this->lexer = clone $this->lexer;
+ $this->query = clone $this->query;
+ $this->queryBuilder = clone $this->queryBuilder;
+ }
+}
diff --git a/app/Core/Filter/OrCriteria.php b/app/Core/Filter/OrCriteria.php
new file mode 100644
index 00000000..174b8458
--- /dev/null
+++ b/app/Core/Filter/OrCriteria.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * OR criteria
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class OrCriteria implements CriteriaInterface
+{
+ /**
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * @var FilterInterface[]
+ */
+ protected $filters = array();
+
+ /**
+ * Set the Query
+ *
+ * @access public
+ * @param Table $query
+ * @return CriteriaInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return CriteriaInterface
+ */
+ public function withFilter(FilterInterface $filter)
+ {
+ $this->filters[] = $filter;
+ return $this;
+ }
+
+ /**
+ * Apply condition
+ *
+ * @access public
+ * @return CriteriaInterface
+ */
+ public function apply()
+ {
+ $this->query->beginOr();
+
+ foreach ($this->filters as $filter) {
+ $filter->withQuery($this->query)->apply();
+ }
+
+ $this->query->closeOr();
+ return $this;
+ }
+}
diff --git a/app/Core/Filter/QueryBuilder.php b/app/Core/Filter/QueryBuilder.php
new file mode 100644
index 00000000..3de82b63
--- /dev/null
+++ b/app/Core/Filter/QueryBuilder.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Core\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Class QueryBuilder
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class QueryBuilder
+{
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * Set the query
+ *
+ * @access public
+ * @param Table $query
+ * @return QueryBuilder
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set a filter
+ *
+ * @access public
+ * @param FilterInterface $filter
+ * @return QueryBuilder
+ */
+ public function withFilter(FilterInterface $filter)
+ {
+ $filter->withQuery($this->query)->apply();
+ return $this;
+ }
+
+ /**
+ * Set a criteria
+ *
+ * @access public
+ * @param CriteriaInterface $criteria
+ * @return QueryBuilder
+ */
+ public function withCriteria(CriteriaInterface $criteria)
+ {
+ $criteria->withQuery($this->query)->apply();
+ return $this;
+ }
+
+ /**
+ * Set a formatter
+ *
+ * @access public
+ * @param FormatterInterface $formatter
+ * @return string|array
+ */
+ public function format(FormatterInterface $formatter)
+ {
+ return $formatter->withQuery($this->query)->format();
+ }
+
+ /**
+ * Get the query result as array
+ *
+ * @access public
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->query->findAll();
+ }
+
+ /**
+ * Get Query object
+ *
+ * @access public
+ * @return Table
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Clone object with deep copy
+ */
+ public function __clone()
+ {
+ $this->query = clone $this->query;
+ }
+}
diff --git a/app/Core/Helper.php b/app/Core/Helper.php
index 3a66fbd0..ab1f8f76 100644
--- a/app/Core/Helper.php
+++ b/app/Core/Helper.php
@@ -12,10 +12,12 @@ use Pimple\Container;
*
* @property \Kanboard\Helper\AppHelper $app
* @property \Kanboard\Helper\AssetHelper $asset
+ * @property \Kanboard\Helper\CalendarHelper $calendar
* @property \Kanboard\Helper\DateHelper $dt
* @property \Kanboard\Helper\FileHelper $file
* @property \Kanboard\Helper\FormHelper $form
* @property \Kanboard\Helper\HookHelper $hook
+ * @property \Kanboard\Helper\ICalHelper $ical
* @property \Kanboard\Helper\ModelHelper $model
* @property \Kanboard\Helper\SubtaskHelper $subtask
* @property \Kanboard\Helper\TaskHelper $task
diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php
index 37349ca5..996fc58d 100644
--- a/app/Core/Http/Response.php
+++ b/app/Core/Http/Response.php
@@ -232,6 +232,20 @@ class Response extends Base
}
/**
+ * Send a iCal response
+ *
+ * @access public
+ * @param string $data Raw data
+ * @param integer $status_code HTTP status code
+ */
+ public function ical($data, $status_code = 200)
+ {
+ $this->status($status_code);
+ $this->contentType('text/calendar; charset=utf-8');
+ echo $data;
+ }
+
+ /**
* Send the security header: Content-Security-Policy
*
* @access public
diff --git a/app/Core/Lexer.php b/app/Core/Lexer.php
deleted file mode 100644
index df2d90ae..00000000
--- a/app/Core/Lexer.php
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-
-namespace Kanboard\Core;
-
-/**
- * Lexer
- *
- * @package core
- * @author Frederic Guillot
- */
-class Lexer
-{
- /**
- * Current position
- *
- * @access private
- * @var integer
- */
- private $offset = 0;
-
- /**
- * Token map
- *
- * @access private
- * @var array
- */
- private $tokenMap = array(
- "/^(assignee:)/" => 'T_ASSIGNEE',
- "/^(color:)/" => 'T_COLOR',
- "/^(due:)/" => 'T_DUE',
- "/^(updated:)/" => 'T_UPDATED',
- "/^(modified:)/" => 'T_UPDATED',
- "/^(created:)/" => 'T_CREATED',
- "/^(status:)/" => 'T_STATUS',
- "/^(description:)/" => 'T_DESCRIPTION',
- "/^(category:)/" => 'T_CATEGORY',
- "/^(column:)/" => 'T_COLUMN',
- "/^(project:)/" => 'T_PROJECT',
- "/^(swimlane:)/" => 'T_SWIMLANE',
- "/^(ref:)/" => 'T_REFERENCE',
- "/^(reference:)/" => 'T_REFERENCE',
- "/^(link:)/" => 'T_LINK',
- "/^(\s+)/" => 'T_WHITESPACE',
- '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE',
- '/^(yesterday|tomorrow|today)/' => 'T_DATE',
- '/^("(.*?)")/' => 'T_STRING',
- "/^(\w+)/" => 'T_STRING',
- "/^(#\d+)/" => 'T_STRING',
- );
-
- /**
- * Tokenize input string
- *
- * @access public
- * @param string $input
- * @return array
- */
- public function tokenize($input)
- {
- $tokens = array();
- $this->offset = 0;
-
- while (isset($input[$this->offset])) {
- $result = $this->match(substr($input, $this->offset));
-
- if ($result === false) {
- return array();
- }
-
- $tokens[] = $result;
- }
-
- return $tokens;
- }
-
- /**
- * Find a token that match and move the offset
- *
- * @access public
- * @param string $string
- * @return array|boolean
- */
- public function match($string)
- {
- foreach ($this->tokenMap as $pattern => $name) {
- if (preg_match($pattern, $string, $matches)) {
- $this->offset += strlen($matches[1]);
-
- return array(
- 'match' => trim($matches[1], '"'),
- 'token' => $name,
- );
- }
- }
-
- return false;
- }
-
- /**
- * Change the output of tokenizer to be easily parsed by the database filter
- *
- * Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title']
- *
- * @access public
- * @param array $tokens
- * @return array
- */
- public function map(array $tokens)
- {
- $map = array(
- 'T_TITLE' => '',
- );
-
- while (false !== ($token = current($tokens))) {
- switch ($token['token']) {
- case 'T_ASSIGNEE':
- case 'T_COLOR':
- case 'T_CATEGORY':
- case 'T_COLUMN':
- case 'T_PROJECT':
- case 'T_SWIMLANE':
- case 'T_LINK':
- $next = next($tokens);
-
- if ($next !== false && $next['token'] === 'T_STRING') {
- $map[$token['token']][] = $next['match'];
- }
-
- break;
-
- case 'T_STATUS':
- case 'T_DUE':
- case 'T_UPDATED':
- case 'T_CREATED':
- case 'T_DESCRIPTION':
- case 'T_REFERENCE':
- $next = next($tokens);
-
- if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) {
- $map[$token['token']] = $next['match'];
- }
-
- break;
-
- default:
- $map['T_TITLE'] .= $token['match'];
- break;
- }
-
- next($tokens);
- }
-
- $map['T_TITLE'] = trim($map['T_TITLE']);
-
- if (empty($map['T_TITLE'])) {
- unset($map['T_TITLE']);
- }
-
- return $map;
- }
-}
diff --git a/app/Filter/BaseFilter.php b/app/Filter/BaseFilter.php
new file mode 100644
index 00000000..a7e6a61a
--- /dev/null
+++ b/app/Filter/BaseFilter.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use PicoDb\Table;
+
+/**
+ * Base filter class
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+abstract class BaseFilter
+{
+ /**
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * @var mixed
+ */
+ protected $value;
+
+ /**
+ * BaseFilter constructor
+ *
+ * @access public
+ * @param mixed $value
+ */
+ public function __construct($value = null)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Get object instance
+ *
+ * @static
+ * @access public
+ * @param mixed $value
+ * @return static
+ */
+ public static function getInstance($value = null)
+ {
+ $self = new static($value);
+ return $self;
+ }
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return \Kanboard\Core\Filter\FilterInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set the value
+ *
+ * @access public
+ * @param string $value
+ * @return \Kanboard\Core\Filter\FilterInterface
+ */
+ public function withValue($value)
+ {
+ $this->value = $value;
+ return $this;
+ }
+
+ /**
+ * Parse operator in the input string
+ *
+ * @access protected
+ * @return string
+ */
+ protected function parseOperator()
+ {
+ $operators = array(
+ '<=' => 'lte',
+ '>=' => 'gte',
+ '<' => 'lt',
+ '>' => 'gt',
+ );
+
+ foreach ($operators as $operator => $method) {
+ if (strpos($this->value, $operator) === 0) {
+ $this->value = substr($this->value, strlen($operator));
+ return $method;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Apply a date filter
+ *
+ * @access protected
+ * @param string $field
+ */
+ protected function applyDateFilter($field)
+ {
+ $timestamp = strtotime($this->value);
+ $method = $this->parseOperator();
+
+ if ($method !== '') {
+ $this->query->$method($field, $timestamp);
+ } else {
+ $this->query->gte($field, $timestamp);
+ $this->query->lte($field, $timestamp + 86399);
+ }
+ }
+}
diff --git a/app/Filter/ProjectGroupRoleProjectFilter.php b/app/Filter/ProjectGroupRoleProjectFilter.php
new file mode 100644
index 00000000..b0950868
--- /dev/null
+++ b/app/Filter/ProjectGroupRoleProjectFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectGroupRole;
+
+/**
+ * Filter ProjectGroupRole users by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectGroupRoleProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectGroupRoleUsernameFilter.php b/app/Filter/ProjectGroupRoleUsernameFilter.php
new file mode 100644
index 00000000..c10855bc
--- /dev/null
+++ b/app/Filter/ProjectGroupRoleUsernameFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\GroupMember;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Model\User;
+
+/**
+ * Filter ProjectGroupRole users by username
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectGroupRoleUsernameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query
+ ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
+ ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
+ ->ilike(User::TABLE.'.username', $this->value.'%');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectIdsFilter.php b/app/Filter/ProjectIdsFilter.php
new file mode 100644
index 00000000..641f7f18
--- /dev/null
+++ b/app/Filter/ProjectIdsFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectIdsFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project_ids');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (empty($this->value)) {
+ $this->query->eq(Project::TABLE.'.id', 0);
+ } else {
+ $this->query->in(Project::TABLE.'.id', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectStatusFilter.php b/app/Filter/ProjectStatusFilter.php
new file mode 100644
index 00000000..a994600c
--- /dev/null
+++ b/app/Filter/ProjectStatusFilter.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Project::TABLE.'.is_active', $this->value);
+ } elseif ($this->value === 'inactive' || $this->value === 'closed' || $this->value === 'disabled') {
+ $this->query->eq(Project::TABLE.'.is_active', 0);
+ } else {
+ $this->query->eq(Project::TABLE.'.is_active', 1);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectTypeFilter.php b/app/Filter/ProjectTypeFilter.php
new file mode 100644
index 00000000..e085e2f6
--- /dev/null
+++ b/app/Filter/ProjectTypeFilter.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+
+/**
+ * Filter project by type
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectTypeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('type');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Project::TABLE.'.is_private', $this->value);
+ } elseif ($this->value === 'private') {
+ $this->query->eq(Project::TABLE.'.is_private', Project::TYPE_PRIVATE);
+ } else {
+ $this->query->eq(Project::TABLE.'.is_private', Project::TYPE_TEAM);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectUserRoleProjectFilter.php b/app/Filter/ProjectUserRoleProjectFilter.php
new file mode 100644
index 00000000..3b880df5
--- /dev/null
+++ b/app/Filter/ProjectUserRoleProjectFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\ProjectUserRole;
+
+/**
+ * Filter ProjectUserRole users by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectUserRoleProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(ProjectUserRole::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/ProjectUserRoleUsernameFilter.php b/app/Filter/ProjectUserRoleUsernameFilter.php
new file mode 100644
index 00000000..c00493a3
--- /dev/null
+++ b/app/Filter/ProjectUserRoleUsernameFilter.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\User;
+
+/**
+ * Filter ProjectUserRole users by username
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class ProjectUserRoleUsernameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query
+ ->join(User::TABLE, 'id', 'user_id')
+ ->ilike(User::TABLE.'.username', $this->value.'%');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskAssigneeFilter.php b/app/Filter/TaskAssigneeFilter.php
new file mode 100644
index 00000000..783d6a12
--- /dev/null
+++ b/app/Filter/TaskAssigneeFilter.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+use Kanboard\Model\User;
+
+/**
+ * Filter tasks by assignee
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskAssigneeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('assignee');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.owner_id', $this->value);
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $this->query->eq(Task::TABLE.'.owner_id', $this->currentUserId);
+ break;
+ case 'nobody':
+ $this->query->eq(Task::TABLE.'.owner_id', 0);
+ break;
+ default:
+ $this->query->beginOr();
+ $this->query->ilike(User::TABLE.'.username', '%'.$this->value.'%');
+ $this->query->ilike(User::TABLE.'.name', '%'.$this->value.'%');
+ $this->query->closeOr();
+ }
+ }
+ }
+}
diff --git a/app/Filter/TaskCategoryFilter.php b/app/Filter/TaskCategoryFilter.php
new file mode 100644
index 00000000..517f24d9
--- /dev/null
+++ b/app/Filter/TaskCategoryFilter.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Category;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by category
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCategoryFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('category');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.category_id', $this->value);
+ } elseif ($this->value === 'none') {
+ $this->query->eq(Task::TABLE.'.category_id', 0);
+ } else {
+ $this->query->eq(Category::TABLE.'.name', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskColorFilter.php b/app/Filter/TaskColorFilter.php
new file mode 100644
index 00000000..784162d4
--- /dev/null
+++ b/app/Filter/TaskColorFilter.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Color;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by color
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskColorFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Color object
+ *
+ * @access private
+ * @var Color
+ */
+ private $colorModel;
+
+ /**
+ * Set color model object
+ *
+ * @access public
+ * @param Color $colorModel
+ * @return TaskColorFilter
+ */
+ public function setColorModel(Color $colorModel)
+ {
+ $this->colorModel = $colorModel;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('color', 'colour');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.color_id', $this->colorModel->find($this->value));
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskColumnFilter.php b/app/Filter/TaskColumnFilter.php
new file mode 100644
index 00000000..9a4d4253
--- /dev/null
+++ b/app/Filter/TaskColumnFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Column;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by column
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskColumnFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('column');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.column_id', $this->value);
+ } else {
+ $this->query->eq(Column::TABLE.'.title', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCompletionDateFilter.php b/app/Filter/TaskCompletionDateFilter.php
new file mode 100644
index 00000000..5166bebf
--- /dev/null
+++ b/app/Filter/TaskCompletionDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by completion date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCompletionDateFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('completed');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_completed');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskCreationDateFilter.php b/app/Filter/TaskCreationDateFilter.php
new file mode 100644
index 00000000..26318b3e
--- /dev/null
+++ b/app/Filter/TaskCreationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by creation date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskCreationDateFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('created');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_creation');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskDescriptionFilter.php b/app/Filter/TaskDescriptionFilter.php
new file mode 100644
index 00000000..6dda58ae
--- /dev/null
+++ b/app/Filter/TaskDescriptionFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by description
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDescriptionFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('description', 'desc');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->ilike(Task::TABLE.'.description', '%'.$this->value.'%');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskDueDateFilter.php b/app/Filter/TaskDueDateFilter.php
new file mode 100644
index 00000000..6ba55eb9
--- /dev/null
+++ b/app/Filter/TaskDueDateFilter.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by due date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDueDateFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('due');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->neq(Task::TABLE.'.date_due', 0);
+ $this->query->notNull(Task::TABLE.'.date_due');
+ $this->applyDateFilter(Task::TABLE.'.date_due');
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskDueDateRangeFilter.php b/app/Filter/TaskDueDateRangeFilter.php
new file mode 100644
index 00000000..10deb0d3
--- /dev/null
+++ b/app/Filter/TaskDueDateRangeFilter.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by due date range
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskDueDateRangeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array();
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->gte(Task::TABLE.'.date_due', is_numeric($this->value[0]) ? $this->value[0] : strtotime($this->value[0]));
+ $this->query->lte(Task::TABLE.'.date_due', is_numeric($this->value[1]) ? $this->value[1] : strtotime($this->value[1]));
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskIdExclusionFilter.php b/app/Filter/TaskIdExclusionFilter.php
new file mode 100644
index 00000000..8bfefb2b
--- /dev/null
+++ b/app/Filter/TaskIdExclusionFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Exclude task ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskIdExclusionFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('exclude');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->notin(Task::TABLE.'.id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskIdFilter.php b/app/Filter/TaskIdFilter.php
new file mode 100644
index 00000000..87bac794
--- /dev/null
+++ b/app/Filter/TaskIdFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by id
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskIdFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('id');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskLinkFilter.php b/app/Filter/TaskLinkFilter.php
new file mode 100644
index 00000000..18a13a09
--- /dev/null
+++ b/app/Filter/TaskLinkFilter.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Link;
+use Kanboard\Model\Task;
+use Kanboard\Model\TaskLink;
+use PicoDb\Database;
+use PicoDb\Table;
+
+/**
+ * Filter tasks by link name
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskLinkFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return TaskLinkFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('link');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ $task_ids = $this->getSubQuery()->findAllByColumn('task_id');
+
+ if (! empty($task_ids)) {
+ $this->query->in(Task::TABLE.'.id', $task_ids);
+ } else {
+ $this->query->eq(Task::TABLE.'.id', 0); // No match
+ }
+ }
+
+ /**
+ * Get subquery
+ *
+ * @access protected
+ * @return Table
+ */
+ protected function getSubQuery()
+ {
+ return $this->db->table(TaskLink::TABLE)
+ ->columns(
+ TaskLink::TABLE.'.task_id',
+ Link::TABLE.'.label'
+ )
+ ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE)
+ ->ilike(Link::TABLE.'.label', $this->value);
+ }
+}
diff --git a/app/Filter/TaskModificationDateFilter.php b/app/Filter/TaskModificationDateFilter.php
new file mode 100644
index 00000000..d8838bce
--- /dev/null
+++ b/app/Filter/TaskModificationDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by modification date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskModificationDateFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('updated', 'modified');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_modification');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskProjectFilter.php b/app/Filter/TaskProjectFilter.php
new file mode 100644
index 00000000..e432efee
--- /dev/null
+++ b/app/Filter/TaskProjectFilter.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by project
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskProjectFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('project');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.project_id', $this->value);
+ } else {
+ $this->query->ilike(Project::TABLE.'.name', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskProjectsFilter.php b/app/Filter/TaskProjectsFilter.php
new file mode 100644
index 00000000..e0fc09cf
--- /dev/null
+++ b/app/Filter/TaskProjectsFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by project ids
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskProjectsFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('projects');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->in(Task::TABLE.'.project_id', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskReferenceFilter.php b/app/Filter/TaskReferenceFilter.php
new file mode 100644
index 00000000..4ad47dd5
--- /dev/null
+++ b/app/Filter/TaskReferenceFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by reference
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskReferenceFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('reference', 'ref');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->eq(Task::TABLE.'.reference', $this->value);
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskStartDateFilter.php b/app/Filter/TaskStartDateFilter.php
new file mode 100644
index 00000000..d45bc0d4
--- /dev/null
+++ b/app/Filter/TaskStartDateFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by start date
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskStartDateFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('started');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->applyDateFilter(Task::TABLE.'.date_started');
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskStatusFilter.php b/app/Filter/TaskStatusFilter.php
new file mode 100644
index 00000000..0ba4361e
--- /dev/null
+++ b/app/Filter/TaskStatusFilter.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by status
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskStatusFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('status');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if ($this->value === 'open' || $this->value === 'closed') {
+ $this->query->eq(Task::TABLE.'.is_active', $this->value === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED);
+ } else {
+ $this->query->eq(Task::TABLE.'.is_active', $this->value);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskSubtaskAssigneeFilter.php b/app/Filter/TaskSubtaskAssigneeFilter.php
new file mode 100644
index 00000000..4c757315
--- /dev/null
+++ b/app/Filter/TaskSubtaskAssigneeFilter.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Subtask;
+use Kanboard\Model\Task;
+use Kanboard\Model\User;
+use PicoDb\Database;
+use PicoDb\Table;
+
+/**
+ * Filter tasks by subtasks assignee
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskSubtaskAssigneeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskSubtaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return TaskSubtaskAssigneeFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('subtask:assignee');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ $task_ids = $this->getSubQuery()->findAllByColumn('task_id');
+
+ if (! empty($task_ids)) {
+ $this->query->in(Task::TABLE.'.id', $task_ids);
+ } else {
+ $this->query->eq(Task::TABLE.'.id', 0); // No match
+ }
+ }
+
+ /**
+ * Get subquery
+ *
+ * @access protected
+ * @return Table
+ */
+ protected function getSubQuery()
+ {
+ $subquery = $this->db->table(Subtask::TABLE)
+ ->columns(
+ Subtask::TABLE.'.user_id',
+ Subtask::TABLE.'.task_id',
+ User::TABLE.'.name',
+ User::TABLE.'.username'
+ )
+ ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE)
+ ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE);
+
+ return $this->applySubQueryFilter($subquery);
+ }
+
+ /**
+ * Apply subquery filter
+ *
+ * @access protected
+ * @param Table $subquery
+ * @return Table
+ */
+ protected function applySubQueryFilter(Table $subquery)
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $subquery->eq(Subtask::TABLE.'.user_id', $this->value);
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $subquery->eq(Subtask::TABLE.'.user_id', $this->currentUserId);
+ break;
+ case 'nobody':
+ $subquery->eq(Subtask::TABLE.'.user_id', 0);
+ break;
+ default:
+ $subquery->beginOr();
+ $subquery->ilike(User::TABLE.'.username', $this->value.'%');
+ $subquery->ilike(User::TABLE.'.name', '%'.$this->value.'%');
+ $subquery->closeOr();
+ }
+ }
+
+ return $subquery;
+ }
+}
diff --git a/app/Filter/TaskSwimlaneFilter.php b/app/Filter/TaskSwimlaneFilter.php
new file mode 100644
index 00000000..4e030244
--- /dev/null
+++ b/app/Filter/TaskSwimlaneFilter.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Project;
+use Kanboard\Model\Swimlane;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by swimlane
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskSwimlaneFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('swimlane');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->eq(Task::TABLE.'.swimlane_id', $this->value);
+ } elseif ($this->value === 'default') {
+ $this->query->eq(Task::TABLE.'.swimlane_id', 0);
+ } else {
+ $this->query->beginOr();
+ $this->query->ilike(Swimlane::TABLE.'.name', $this->value);
+ $this->query->ilike(Project::TABLE.'.default_swimlane', $this->value);
+ $this->query->closeOr();
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/TaskTitleFilter.php b/app/Filter/TaskTitleFilter.php
new file mode 100644
index 00000000..9853369c
--- /dev/null
+++ b/app/Filter/TaskTitleFilter.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Filter tasks by title
+ *
+ * @package filter
+ * @author Frederic Guillot
+ */
+class TaskTitleFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('title');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ if (ctype_digit($this->value) || (strlen($this->value) > 1 && $this->value{0} === '#' && ctype_digit(substr($this->value, 1)))) {
+ $this->query->beginOr();
+ $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $this->value));
+ $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%');
+ $this->query->closeOr();
+ } else {
+ $this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%');
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Filter/UserNameFilter.php b/app/Filter/UserNameFilter.php
new file mode 100644
index 00000000..dfb07fdd
--- /dev/null
+++ b/app/Filter/UserNameFilter.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Kanboard\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+
+class UserNameFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('name');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return FilterInterface
+ */
+ public function apply()
+ {
+ $this->query->beginOr()
+ ->ilike('username', '%'.$this->value.'%')
+ ->ilike('name', '%'.$this->value.'%')
+ ->closeOr();
+
+ return $this;
+ }
+}
diff --git a/app/Formatter/BaseFormatter.php b/app/Formatter/BaseFormatter.php
new file mode 100644
index 00000000..a9f0ad15
--- /dev/null
+++ b/app/Formatter/BaseFormatter.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\FormatterInterface;
+use PicoDb\Table;
+
+/**
+ * Class BaseFormatter
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+abstract class BaseFormatter extends Base
+{
+ /**
+ * Query object
+ *
+ * @access protected
+ * @var Table
+ */
+ protected $query;
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+}
diff --git a/app/Formatter/TaskFilterCalendarEvent.php b/app/Formatter/BaseTaskCalendarFormatter.php
index 12ea8687..8fab3e9a 100644
--- a/app/Formatter/TaskFilterCalendarEvent.php
+++ b/app/Formatter/BaseTaskCalendarFormatter.php
@@ -2,7 +2,7 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
* Common class to handle calendar events
@@ -10,7 +10,7 @@ use Kanboard\Model\TaskFilter;
* @package formatter
* @author Frederic Guillot
*/
-abstract class TaskFilterCalendarEvent extends TaskFilter
+abstract class BaseTaskCalendarFormatter extends BaseFormatter
{
/**
* Column used for event start date
@@ -29,20 +29,12 @@ abstract class TaskFilterCalendarEvent extends TaskFilter
protected $endColumn = 'date_completed';
/**
- * Full day event flag
- *
- * @access private
- * @var boolean
- */
- private $fullDay = false;
-
- /**
* Transform results to calendar events
*
* @access public
* @param string $start_column Column name for the start date
* @param string $end_column Column name for the end date
- * @return TaskFilterCalendarEvent
+ * @return FormatterInterface
*/
public function setColumns($start_column, $end_column = '')
{
@@ -50,27 +42,4 @@ abstract class TaskFilterCalendarEvent extends TaskFilter
$this->endColumn = $end_column ?: $start_column;
return $this;
}
-
- /**
- * When called calendar events will be full day
- *
- * @access public
- * @return TaskFilterCalendarEvent
- */
- public function setFullDay()
- {
- $this->fullDay = true;
- return $this;
- }
-
- /**
- * Return true if the events are full day
- *
- * @access public
- * @return boolean
- */
- public function isFullDay()
- {
- return $this->fullDay;
- }
}
diff --git a/app/Formatter/BoardFormatter.php b/app/Formatter/BoardFormatter.php
new file mode 100644
index 00000000..6a96b3e6
--- /dev/null
+++ b/app/Formatter/BoardFormatter.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Filter\FormatterInterface;
+use Kanboard\Model\Task;
+
+/**
+ * Board Formatter
+ *
+ * @package formatter
+ * @author Frederic Guillot
+ */
+class BoardFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Project id
+ *
+ * @access protected
+ * @var integer
+ */
+ protected $projectId;
+
+ /**
+ * Set ProjectId
+ *
+ * @access public
+ * @param integer $projectId
+ * @return $this
+ */
+ public function setProjectId($projectId)
+ {
+ $this->projectId = $projectId;
+ return $this;
+ }
+
+ /**
+ * Apply formatter
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $tasks = $this->query
+ ->eq(Task::TABLE.'.project_id', $this->projectId)
+ ->asc(Task::TABLE.'.position')
+ ->findAll();
+
+ return $this->board->getBoard($this->projectId, function ($project_id, $column_id, $swimlane_id) use ($tasks) {
+ return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) {
+ return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id;
+ });
+ });
+ }
+}
diff --git a/app/Formatter/FormatterInterface.php b/app/Formatter/FormatterInterface.php
deleted file mode 100644
index 0bb61292..00000000
--- a/app/Formatter/FormatterInterface.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Kanboard\Formatter;
-
-/**
- * Formatter Interface
- *
- * @package formatter
- * @author Frederic Guillot
- */
-interface FormatterInterface
-{
- public function format();
-}
diff --git a/app/Formatter/GroupAutoCompleteFormatter.php b/app/Formatter/GroupAutoCompleteFormatter.php
index 7023e367..4d552886 100644
--- a/app/Formatter/GroupAutoCompleteFormatter.php
+++ b/app/Formatter/GroupAutoCompleteFormatter.php
@@ -2,8 +2,12 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
+use Kanboard\Core\Group\GroupProviderInterface;
+use PicoDb\Table;
+
/**
- * Autocomplete formatter for groups
+ * Auto-complete formatter for groups
*
* @package formatter
* @author Frederic Guillot
@@ -14,25 +18,35 @@ class GroupAutoCompleteFormatter implements FormatterInterface
* Groups found
*
* @access private
- * @var array
+ * @var GroupProviderInterface[]
*/
private $groups;
/**
- * Format groups for the ajax autocompletion
+ * Format groups for the ajax auto-completion
*
* @access public
- * @param array $groups
- * @return GroupAutoCompleteFormatter
+ * @param GroupProviderInterface[] $groups
*/
- public function setGroups(array $groups)
+ public function __construct(array $groups)
{
$this->groups = $groups;
+ }
+
+ /**
+ * Set query
+ *
+ * @access public
+ * @param Table $query
+ * @return FormatterInterface
+ */
+ public function withQuery(Table $query)
+ {
return $this;
}
/**
- * Format groups for the ajax autocompletion
+ * Format groups for the ajax auto-completion
*
* @access public
* @return array
diff --git a/app/Formatter/ProjectGanttFormatter.php b/app/Formatter/ProjectGanttFormatter.php
index 4f73e217..aee1f27f 100644
--- a/app/Formatter/ProjectGanttFormatter.php
+++ b/app/Formatter/ProjectGanttFormatter.php
@@ -2,7 +2,7 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\Project;
+use Kanboard\Core\Filter\FormatterInterface;
/**
* Gantt chart formatter for projects
@@ -10,41 +10,9 @@ use Kanboard\Model\Project;
* @package formatter
* @author Frederic Guillot
*/
-class ProjectGanttFormatter extends Project implements FormatterInterface
+class ProjectGanttFormatter extends BaseFormatter implements FormatterInterface
{
/**
- * List of projects
- *
- * @access private
- * @var array
- */
- private $projects = array();
-
- /**
- * Filter projects to generate the Gantt chart
- *
- * @access public
- * @param int[] $project_ids
- * @return ProjectGanttFormatter
- */
- public function filter(array $project_ids)
- {
- if (empty($project_ids)) {
- $this->projects = array();
- } else {
- $this->projects = $this->db
- ->table(self::TABLE)
- ->asc('start_date')
- ->in('id', $project_ids)
- ->eq('is_active', self::ACTIVE)
- ->eq('is_private', 0)
- ->findAll();
- }
-
- return $this;
- }
-
- /**
* Format projects to be displayed in the Gantt chart
*
* @access public
@@ -52,10 +20,11 @@ class ProjectGanttFormatter extends Project implements FormatterInterface
*/
public function format()
{
+ $projects = $this->query->findAll();
$colors = $this->color->getDefaultColors();
$bars = array();
- foreach ($this->projects as $project) {
+ foreach ($projects as $project) {
$start = empty($project['start_date']) ? time() : strtotime($project['start_date']);
$end = empty($project['end_date']) ? $start : strtotime($project['end_date']);
$color = next($colors) ?: reset($colors);
diff --git a/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php b/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php
new file mode 100644
index 00000000..c5d4e2be
--- /dev/null
+++ b/app/Formatter/SubtaskTimeTrackingCalendarFormatter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Formatter;
+
+use Kanboard\Core\Filter\FormatterInterface;
+
+class SubtaskTimeTrackingCalendarFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Format calendar events
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $events = array();
+
+ foreach ($this->query->findAll() as $row) {
+ $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
+
+ $events[] = array(
+ 'id' => $row['id'],
+ 'subtask_id' => $row['subtask_id'],
+ 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
+ 'start' => date('Y-m-d\TH:i:s', $row['start']),
+ 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
+ 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
+ 'borderColor' => $this->color->getBorderColor($row['color_id']),
+ 'textColor' => 'black',
+ 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
+ 'editable' => false,
+ );
+ }
+
+ return $events;
+ }
+}
diff --git a/app/Formatter/TaskFilterAutoCompleteFormatter.php b/app/Formatter/TaskAutoCompleteFormatter.php
index c9af4654..480ee797 100644
--- a/app/Formatter/TaskFilterAutoCompleteFormatter.php
+++ b/app/Formatter/TaskAutoCompleteFormatter.php
@@ -2,19 +2,19 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
use Kanboard\Model\Task;
-use Kanboard\Model\TaskFilter;
/**
- * Autocomplete formatter for task filter
+ * Task AutoComplete Formatter
*
- * @package formatter
- * @author Frederic Guillot
+ * @package formatter
+ * @author Frederic Guillot
*/
-class TaskFilterAutoCompleteFormatter extends TaskFilter implements FormatterInterface
+class TaskAutoCompleteFormatter extends BaseFormatter implements FormatterInterface
{
/**
- * Format the tasks for the ajax autocompletion
+ * Apply formatter
*
* @access public
* @return array
diff --git a/app/Formatter/TaskFilterCalendarFormatter.php b/app/Formatter/TaskCalendarFormatter.php
index 1b5d6ca4..60b9a062 100644
--- a/app/Formatter/TaskFilterCalendarFormatter.php
+++ b/app/Formatter/TaskCalendarFormatter.php
@@ -2,15 +2,37 @@
namespace Kanboard\Formatter;
+use Kanboard\Core\Filter\FormatterInterface;
+
/**
* Calendar event formatter for task filter
*
* @package formatter
* @author Frederic Guillot
*/
-class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface
+class TaskCalendarFormatter extends BaseTaskCalendarFormatter implements FormatterInterface
{
/**
+ * Full day event flag
+ *
+ * @access private
+ * @var boolean
+ */
+ private $fullDay = false;
+
+ /**
+ * When called calendar events will be full day
+ *
+ * @access public
+ * @return FormatterInterface
+ */
+ public function setFullDay()
+ {
+ $this->fullDay = true;
+ return $this;
+ }
+
+ /**
* Transform tasks to calendar events
*
* @access public
@@ -31,8 +53,8 @@ class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements For
'url' => $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
'start' => date($this->getDateTimeFormat(), $task[$this->startColumn]),
'end' => date($this->getDateTimeFormat(), $task[$this->endColumn] ?: time()),
- 'editable' => $this->isFullDay(),
- 'allday' => $this->isFullDay(),
+ 'editable' => $this->fullDay,
+ 'allday' => $this->fullDay,
);
}
@@ -47,6 +69,6 @@ class TaskFilterCalendarFormatter extends TaskFilterCalendarEvent implements For
*/
private function getDateTimeFormat()
{
- return $this->isFullDay() ? 'Y-m-d' : 'Y-m-d\TH:i:s';
+ return $this->fullDay ? 'Y-m-d' : 'Y-m-d\TH:i:s';
}
}
diff --git a/app/Formatter/TaskFilterGanttFormatter.php b/app/Formatter/TaskGanttFormatter.php
index a4eef1ee..3209aa37 100644
--- a/app/Formatter/TaskFilterGanttFormatter.php
+++ b/app/Formatter/TaskGanttFormatter.php
@@ -2,15 +2,15 @@
namespace Kanboard\Formatter;
-use Kanboard\Model\TaskFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * Gantt chart formatter for task filter
+ * Task Gantt Formatter
*
- * @package formatter
- * @author Frederic Guillot
+ * @package formatter
+ * @author Frederic Guillot
*/
-class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface
+class TaskGanttFormatter extends BaseFormatter implements FormatterInterface
{
/**
* Local cache for project columns
@@ -19,9 +19,9 @@ class TaskFilterGanttFormatter extends TaskFilter implements FormatterInterface
* @var array
*/
private $columns = array();
-
+
/**
- * Format tasks to be displayed in the Gantt chart
+ * Apply formatter
*
* @access public
* @return array
diff --git a/app/Formatter/TaskFilterICalendarFormatter.php b/app/Formatter/TaskICalFormatter.php
index 25b3aea0..a149f725 100644
--- a/app/Formatter/TaskFilterICalendarFormatter.php
+++ b/app/Formatter/TaskICalFormatter.php
@@ -6,14 +6,15 @@ use DateTime;
use Eluceo\iCal\Component\Calendar;
use Eluceo\iCal\Component\Event;
use Eluceo\iCal\Property\Event\Attendees;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * iCal event formatter for task filter
+ * iCal event formatter for tasks
*
* @package formatter
* @author Frederic Guillot
*/
-class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements FormatterInterface
+class TaskICalFormatter extends BaseTaskCalendarFormatter implements FormatterInterface
{
/**
* Calendar object
@@ -39,7 +40,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
*
* @access public
* @param \Eluceo\iCal\Component\Calendar $vCalendar
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function setCalendar(Calendar $vCalendar)
{
@@ -48,10 +49,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Transform results to ical events
+ * Transform results to iCal events
*
* @access public
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function addDateTimeEvents()
{
@@ -73,10 +74,10 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Transform results to all day ical events
+ * Transform results to all day iCal events
*
* @access public
- * @return TaskFilterICalendarFormatter
+ * @return FormatterInterface
*/
public function addFullDayEvents()
{
@@ -96,7 +97,7 @@ class TaskFilterICalendarFormatter extends TaskFilterCalendarEvent implements Fo
}
/**
- * Get common events for task ical events
+ * Get common events for task iCal events
*
* @access protected
* @param array $task
diff --git a/app/Formatter/UserFilterAutoCompleteFormatter.php b/app/Formatter/UserAutoCompleteFormatter.php
index b98e0d69..c46a24d0 100644
--- a/app/Formatter/UserFilterAutoCompleteFormatter.php
+++ b/app/Formatter/UserAutoCompleteFormatter.php
@@ -3,15 +3,15 @@
namespace Kanboard\Formatter;
use Kanboard\Model\User;
-use Kanboard\Model\UserFilter;
+use Kanboard\Core\Filter\FormatterInterface;
/**
- * Autocomplete formatter for user filter
+ * Auto-complete formatter for user filter
*
* @package formatter
* @author Frederic Guillot
*/
-class UserFilterAutoCompleteFormatter extends UserFilter implements FormatterInterface
+class UserAutoCompleteFormatter extends BaseFormatter implements FormatterInterface
{
/**
* Format the tasks for the ajax autocompletion
diff --git a/app/Helper/CalendarHelper.php b/app/Helper/CalendarHelper.php
new file mode 100644
index 00000000..d5f4af21
--- /dev/null
+++ b/app/Helper/CalendarHelper.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskDueDateRangeFilter;
+use Kanboard\Formatter\SubtaskTimeTrackingCalendarFormatter;
+use Kanboard\Formatter\TaskCalendarFormatter;
+
+/**
+ * Calendar Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class CalendarHelper extends Base
+{
+ /**
+ * Get formatted calendar task due events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getTaskDateDueEvents(QueryBuilder $queryBuilder, $start, $end)
+ {
+ $formatter = new TaskCalendarFormatter($this->container);
+ $formatter->setFullDay();
+ $formatter->setColumns('date_due');
+
+ return $queryBuilder
+ ->withFilter(new TaskDueDateRangeFilter(array($start, $end)))
+ ->format($formatter);
+ }
+
+ /**
+ * Get formatted calendar task events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getTaskEvents(QueryBuilder $queryBuilder, $start, $end)
+ {
+ $startColumn = $this->config->get('calendar_project_tasks', 'date_started');
+
+ $queryBuilder->getQuery()->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ $startColumn,
+ 'date_due'
+ ));
+
+ $formatter = new TaskCalendarFormatter($this->container);
+ $formatter->setColumns($startColumn, 'date_due');
+
+ return $queryBuilder->format($formatter);
+ }
+
+ /**
+ * Get formatted calendar subtask time tracking events
+ *
+ * @access public
+ * @param integer $user_id
+ * @param string $start
+ * @param string $end
+ * @return array
+ */
+ public function getSubtaskTimeTrackingEvents($user_id, $start, $end)
+ {
+ $formatter = new SubtaskTimeTrackingCalendarFormatter($this->container);
+ return $formatter
+ ->withQuery($this->subtaskTimeTracking->getUserQuery($user_id)
+ ->addCondition($this->getCalendarCondition(
+ $this->dateParser->getTimestampFromIsoFormat($start),
+ $this->dateParser->getTimestampFromIsoFormat($end),
+ 'start',
+ 'end'
+ ))
+ )
+ ->format();
+ }
+
+ /**
+ * Build SQL condition for a given time range
+ *
+ * @access public
+ * @param string $start_time Start timestamp
+ * @param string $end_time End timestamp
+ * @param string $start_column Start column name
+ * @param string $end_column End column name
+ * @return string
+ */
+ public function getCalendarCondition($start_time, $end_time, $start_column, $end_column)
+ {
+ $start_column = $this->db->escapeIdentifier($start_column);
+ $end_column = $this->db->escapeIdentifier($end_column);
+
+ $conditions = array(
+ "($start_column >= '$start_time' AND $start_column <= '$end_time')",
+ "($start_column <= '$start_time' AND $end_column >= '$start_time')",
+ "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))",
+ );
+
+ return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')';
+ }
+}
diff --git a/app/Helper/ICalHelper.php b/app/Helper/ICalHelper.php
new file mode 100644
index 00000000..dc399bf8
--- /dev/null
+++ b/app/Helper/ICalHelper.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Kanboard\Helper;
+
+use Kanboard\Core\Base;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskDueDateRangeFilter;
+use Kanboard\Formatter\TaskICalFormatter;
+use Eluceo\iCal\Component\Calendar as iCalendar;
+
+/**
+ * ICal Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class ICalHelper extends Base
+{
+ /**
+ * Get formatted calendar task due events
+ *
+ * @access public
+ * @param QueryBuilder $queryBuilder
+ * @param iCalendar $calendar
+ * @param string $start
+ * @param string $end
+ */
+ public function addTaskDateDueEvents(QueryBuilder $queryBuilder, iCalendar $calendar, $start, $end)
+ {
+ $queryBuilder->withFilter(new TaskDueDateRangeFilter(array($start, $end)));
+
+ $formatter = new TaskICalFormatter($this->container);
+ $formatter->setColumns('date_due');
+ $formatter->setCalendar($calendar);
+ $formatter->withQuery($queryBuilder->getQuery());
+ $formatter->addFullDayEvents();
+ }
+}
diff --git a/app/Model/AvatarFile.php b/app/Model/AvatarFile.php
index 52d07962..c49f9fd5 100644
--- a/app/Model/AvatarFile.php
+++ b/app/Model/AvatarFile.php
@@ -76,6 +76,7 @@ class AvatarFile extends Base
* @access public
* @param integer $user_id
* @param array $file
+ * @return boolean
*/
public function uploadFile($user_id, array $file)
{
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 714b4308..a27560c8 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -31,28 +31,4 @@ abstract class Base extends \Kanboard\Core\Base
return (int) $db->getLastId();
});
}
-
- /**
- * Build SQL condition for a given time range
- *
- * @access protected
- * @param string $start_time Start timestamp
- * @param string $end_time End timestamp
- * @param string $start_column Start column name
- * @param string $end_column End column name
- * @return string
- */
- protected function getCalendarCondition($start_time, $end_time, $start_column, $end_column)
- {
- $start_column = $this->db->escapeIdentifier($start_column);
- $end_column = $this->db->escapeIdentifier($end_column);
-
- $conditions = array(
- "($start_column >= '$start_time' AND $start_column <= '$end_time')",
- "($start_column <= '$start_time' AND $end_column >= '$start_time')",
- "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))",
- );
-
- return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')';
- }
}
diff --git a/app/Model/Project.php b/app/Model/Project.php
index d2e5b7ce..6e3c2326 100644
--- a/app/Model/Project.php
+++ b/app/Model/Project.php
@@ -35,6 +35,20 @@ class Project extends Base
const INACTIVE = 0;
/**
+ * Value for private project
+ *
+ * @var integer
+ */
+ const TYPE_PRIVATE = 1;
+
+ /**
+ * Value for team project
+ *
+ * @var integer
+ */
+ const TYPE_TEAM = 0;
+
+ /**
* Get a project by the id
*
* @access public
diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php
index d399d5c6..34893f0b 100644
--- a/app/Model/ProjectActivity.php
+++ b/app/Model/ProjectActivity.php
@@ -2,6 +2,8 @@
namespace Kanboard\Model;
+use PicoDb\Table;
+
/**
* Project activity model
*
@@ -133,12 +135,12 @@ class ProjectActivity extends Base
* Common function to return events
*
* @access public
- * @param \PicoDb\Table $query PicoDb Query
+ * @param Table $query PicoDb Query
* @param integer $start Timestamp of earliest activity
* @param integer $end Timestamp of latest activity
* @return array
*/
- private function getEvents(\PicoDb\Table $query, $start, $end)
+ private function getEvents(Table $query, $start, $end)
{
if (! is_null($start)) {
$query->gte('date_creation', $start);
diff --git a/app/Model/ProjectGroupRoleFilter.php b/app/Model/ProjectGroupRoleFilter.php
deleted file mode 100644
index 989d3073..00000000
--- a/app/Model/ProjectGroupRoleFilter.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Project Group Role Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class ProjectGroupRoleFilter extends Base
-{
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @return UserFilter
- */
- public function create()
- {
- $this->query = $this->db->table(ProjectGroupRole::TABLE);
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @param string $column
- * @return array
- */
- public function findAll($column = '')
- {
- if ($column !== '') {
- return $this->query->asc($column)->findAllByColumn($column);
- }
-
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return ProjectUserRoleFilter
- */
- public function filterByProjectId($project_id)
- {
- $this->query->eq(ProjectGroupRole::TABLE.'.project_id', $project_id);
- return $this;
- }
-
- /**
- * Filter by username
- *
- * @access public
- * @param string $input
- * @return ProjectUserRoleFilter
- */
- public function startWithUsername($input)
- {
- $this->query
- ->join(GroupMember::TABLE, 'group_id', 'group_id', ProjectGroupRole::TABLE)
- ->join(User::TABLE, 'id', 'user_id', GroupMember::TABLE)
- ->ilike(User::TABLE.'.username', $input.'%');
-
- return $this;
- }
-}
diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php
index db1573ae..59af2b58 100644
--- a/app/Model/ProjectPermission.php
+++ b/app/Model/ProjectPermission.php
@@ -3,6 +3,10 @@
namespace Kanboard\Model;
use Kanboard\Core\Security\Role;
+use Kanboard\Filter\ProjectGroupRoleProjectFilter;
+use Kanboard\Filter\ProjectGroupRoleUsernameFilter;
+use Kanboard\Filter\ProjectUserRoleProjectFilter;
+use Kanboard\Filter\ProjectUserRoleUsernameFilter;
/**
* Project Permission
@@ -53,8 +57,18 @@ class ProjectPermission extends Base
*/
public function findUsernames($project_id, $input)
{
- $userMembers = $this->projectUserRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
- $groupMembers = $this->projectGroupRoleFilter->create()->filterByProjectId($project_id)->startWithUsername($input)->findAll('username');
+ $userMembers = $this->projectUserRoleQuery
+ ->withFilter(new ProjectUserRoleProjectFilter($project_id))
+ ->withFilter(new ProjectUserRoleUsernameFilter($input))
+ ->getQuery()
+ ->findAllByColumn('username');
+
+ $groupMembers = $this->projectGroupRoleQuery
+ ->withFilter(new ProjectGroupRoleProjectFilter($project_id))
+ ->withFilter(new ProjectGroupRoleUsernameFilter($input))
+ ->getQuery()
+ ->findAllByColumn('username');
+
$members = array_unique(array_merge($userMembers, $groupMembers));
sort($members);
diff --git a/app/Model/ProjectUserRole.php b/app/Model/ProjectUserRole.php
index 56da679c..2956c524 100644
--- a/app/Model/ProjectUserRole.php
+++ b/app/Model/ProjectUserRole.php
@@ -251,8 +251,8 @@ class ProjectUserRole extends Base
/**
* Copy user access from a project to another one
*
- * @param integer $project_src_id Project Template
- * @return integer $project_dst_id Project that receives the copy
+ * @param integer $project_src_id
+ * @param integer $project_dst_id
* @return boolean
*/
public function duplicate($project_src_id, $project_dst_id)
diff --git a/app/Model/ProjectUserRoleFilter.php b/app/Model/ProjectUserRoleFilter.php
deleted file mode 100644
index 64403643..00000000
--- a/app/Model/ProjectUserRoleFilter.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Project User Role Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class ProjectUserRoleFilter extends Base
-{
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @return UserFilter
- */
- public function create()
- {
- $this->query = $this->db->table(ProjectUserRole::TABLE);
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @param string $column
- * @return array
- */
- public function findAll($column = '')
- {
- if ($column !== '') {
- return $this->query->asc($column)->findAllByColumn($column);
- }
-
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return ProjectUserRoleFilter
- */
- public function filterByProjectId($project_id)
- {
- $this->query->eq(ProjectUserRole::TABLE.'.project_id', $project_id);
- return $this;
- }
-
- /**
- * Filter by username
- *
- * @access public
- * @param string $input
- * @return ProjectUserRoleFilter
- */
- public function startWithUsername($input)
- {
- $this->query
- ->join(User::TABLE, 'id', 'user_id')
- ->ilike(User::TABLE.'.username', $input.'%');
-
- return $this;
- }
-}
diff --git a/app/Model/Setting.php b/app/Model/Setting.php
index f98d7ce1..c5a4765c 100644
--- a/app/Model/Setting.php
+++ b/app/Model/Setting.php
@@ -22,6 +22,7 @@ abstract class Setting extends Base
*
* @abstract
* @access public
+ * @param array $values
* @return array
*/
abstract public function prepare(array $values);
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
index b766b542..be04ee1b 100644
--- a/app/Model/SubtaskTimeTracking.php
+++ b/app/Model/SubtaskTimeTracking.php
@@ -146,94 +146,6 @@ class SubtaskTimeTracking extends Base
}
/**
- * Get user calendar events
- *
- * @access public
- * @param integer $user_id
- * @param string $start ISO-8601 format
- * @param string $end
- * @return array
- */
- public function getUserCalendarEvents($user_id, $start, $end)
- {
- $hook = 'model:subtask-time-tracking:calendar:events';
- $events = $this->getUserQuery($user_id)
- ->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'start',
- 'end'
- ))
- ->findAll();
-
- if ($this->hook->exists($hook)) {
- $events = $this->hook->first($hook, array(
- 'user_id' => $user_id,
- 'events' => $events,
- 'start' => $start,
- 'end' => $end,
- ));
- }
-
- return $this->toCalendarEvents($events);
- }
-
- /**
- * Get project calendar events
- *
- * @access public
- * @param integer $project_id
- * @param integer $start
- * @param integer $end
- * @return array
- */
- public function getProjectCalendarEvents($project_id, $start, $end)
- {
- $result = $this
- ->getProjectQuery($project_id)
- ->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'start',
- 'end'
- ))
- ->findAll();
-
- return $this->toCalendarEvents($result);
- }
-
- /**
- * Convert a record set to calendar events
- *
- * @access private
- * @param array $rows
- * @return array
- */
- private function toCalendarEvents(array $rows)
- {
- $events = array();
-
- foreach ($rows as $row) {
- $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
-
- $events[] = array(
- 'id' => $row['id'],
- 'subtask_id' => $row['subtask_id'],
- 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
- 'start' => date('Y-m-d\TH:i:s', $row['start']),
- 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
- 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
- 'borderColor' => $this->color->getBorderColor($row['color_id']),
- 'textColor' => 'black',
- 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
- 'editable' => false,
- );
- }
-
- return $events;
- }
-
- /**
* Return true if a timer is started for this use and subtask
*
* @access public
diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php
deleted file mode 100644
index 1883298d..00000000
--- a/app/Model/TaskFilter.php
+++ /dev/null
@@ -1,745 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * Task Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class TaskFilter extends Base
-{
- /**
- * Filters mapping
- *
- * @access private
- * @var array
- */
- private $filters = array(
- 'T_ASSIGNEE' => 'filterByAssignee',
- 'T_COLOR' => 'filterByColors',
- 'T_DUE' => 'filterByDueDate',
- 'T_UPDATED' => 'filterByModificationDate',
- 'T_CREATED' => 'filterByCreationDate',
- 'T_TITLE' => 'filterByTitle',
- 'T_STATUS' => 'filterByStatusName',
- 'T_DESCRIPTION' => 'filterByDescription',
- 'T_CATEGORY' => 'filterByCategoryName',
- 'T_PROJECT' => 'filterByProjectName',
- 'T_COLUMN' => 'filterByColumnName',
- 'T_REFERENCE' => 'filterByReference',
- 'T_SWIMLANE' => 'filterBySwimlaneName',
- 'T_LINK' => 'filterByLinkName',
- );
-
- /**
- * Query
- *
- * @access public
- * @var \PicoDb\Table
- */
- public $query;
-
- /**
- * Apply filters according to the search input
- *
- * @access public
- * @param string $input
- * @return TaskFilter
- */
- public function search($input)
- {
- $tree = $this->lexer->map($this->lexer->tokenize($input));
- $this->query = $this->taskFinder->getExtendedQuery();
-
- if (empty($tree)) {
- $this->filterByTitle($input);
- }
-
- foreach ($tree as $filter => $value) {
- $method = $this->filters[$filter];
- $this->$method($value);
- }
-
- return $this;
- }
-
- /**
- * Create a new query
- *
- * @access public
- * @return TaskFilter
- */
- public function create()
- {
- $this->query = $this->db->table(Task::TABLE);
- $this->query->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id');
- $this->query->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id');
-
- $this->query->columns(
- Task::TABLE.'.*',
- 'ua.email AS assignee_email',
- 'ua.name AS assignee_name',
- 'ua.username AS assignee_username',
- 'uc.email AS creator_email',
- 'uc.username AS creator_username'
- );
-
- return $this;
- }
-
- /**
- * Create a new subtask query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function createSubtaskQuery()
- {
- return $this->db->table(Subtask::TABLE)
- ->columns(
- Subtask::TABLE.'.user_id',
- Subtask::TABLE.'.task_id',
- User::TABLE.'.name',
- User::TABLE.'.username'
- )
- ->join(User::TABLE, 'id', 'user_id', Subtask::TABLE)
- ->neq(Subtask::TABLE.'.status', Subtask::STATUS_DONE);
- }
-
- /**
- * Create a new link query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function createLinkQuery()
- {
- return $this->db->table(TaskLink::TABLE)
- ->columns(
- TaskLink::TABLE.'.task_id',
- Link::TABLE.'.label'
- )
- ->join(Link::TABLE, 'id', 'link_id', TaskLink::TABLE);
- }
-
- /**
- * Clone the filter
- *
- * @access public
- * @return TaskFilter
- */
- public function copy()
- {
- $filter = new static($this->container);
- $filter->query = clone($this->query);
- $filter->query->condition = clone($this->query->condition);
- return $filter;
- }
-
- /**
- * Exclude a list of task_id
- *
- * @access public
- * @param integer[] $task_ids
- * @return TaskFilter
- */
- public function excludeTasks(array $task_ids)
- {
- $this->query->notin(Task::TABLE.'.id', $task_ids);
- return $this;
- }
-
- /**
- * Filter by id
- *
- * @access public
- * @param integer $task_id
- * @return TaskFilter
- */
- public function filterById($task_id)
- {
- if ($task_id > 0) {
- $this->query->eq(Task::TABLE.'.id', $task_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by reference
- *
- * @access public
- * @param string $reference
- * @return TaskFilter
- */
- public function filterByReference($reference)
- {
- if (! empty($reference)) {
- $this->query->eq(Task::TABLE.'.reference', $reference);
- }
-
- return $this;
- }
-
- /**
- * Filter by title
- *
- * @access public
- * @param string $title
- * @return TaskFilter
- */
- public function filterByDescription($title)
- {
- $this->query->ilike(Task::TABLE.'.description', '%'.$title.'%');
- return $this;
- }
-
- /**
- * Filter by title or id if the string is like #123 or an integer
- *
- * @access public
- * @param string $title
- * @return TaskFilter
- */
- public function filterByTitle($title)
- {
- if (ctype_digit($title) || (strlen($title) > 1 && $title{0} === '#' && ctype_digit(substr($title, 1)))) {
- $this->query->beginOr();
- $this->query->eq(Task::TABLE.'.id', str_replace('#', '', $title));
- $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%');
- $this->query->closeOr();
- } else {
- $this->query->ilike(Task::TABLE.'.title', '%'.$title.'%');
- }
-
- return $this;
- }
-
- /**
- * Filter by a list of project id
- *
- * @access public
- * @param array $project_ids
- * @return TaskFilter
- */
- public function filterByProjects(array $project_ids)
- {
- $this->query->in(Task::TABLE.'.project_id', $project_ids);
- return $this;
- }
-
- /**
- * Filter by project id
- *
- * @access public
- * @param integer $project_id
- * @return TaskFilter
- */
- public function filterByProject($project_id)
- {
- if ($project_id > 0) {
- $this->query->eq(Task::TABLE.'.project_id', $project_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by project name
- *
- * @access public
- * @param array $values List of project name
- * @return TaskFilter
- */
- public function filterByProjectName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $project) {
- if (ctype_digit($project)) {
- $this->query->eq(Task::TABLE.'.project_id', $project);
- } else {
- $this->query->ilike(Project::TABLE.'.name', $project);
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by swimlane name
- *
- * @access public
- * @param array $values List of swimlane name
- * @return TaskFilter
- */
- public function filterBySwimlaneName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $swimlane) {
- if ($swimlane === 'default') {
- $this->query->eq(Task::TABLE.'.swimlane_id', 0);
- } else {
- $this->query->ilike(Swimlane::TABLE.'.name', $swimlane);
- $this->query->addCondition(Task::TABLE.'.swimlane_id=0 AND '.Project::TABLE.'.default_swimlane '.$this->db->getDriver()->getOperator('ILIKE')." '$swimlane'");
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by category id
- *
- * @access public
- * @param integer $category_id
- * @return TaskFilter
- */
- public function filterByCategory($category_id)
- {
- if ($category_id >= 0) {
- $this->query->eq(Task::TABLE.'.category_id', $category_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by category
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterByCategoryName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $category) {
- if ($category === 'none') {
- $this->query->eq(Task::TABLE.'.category_id', 0);
- } else {
- $this->query->eq(Category::TABLE.'.name', $category);
- }
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by assignee
- *
- * @access public
- * @param integer $owner_id
- * @return TaskFilter
- */
- public function filterByOwner($owner_id)
- {
- if ($owner_id >= 0) {
- $this->query->eq(Task::TABLE.'.owner_id', $owner_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by assignee names
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterByAssignee(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $assignee) {
- switch ($assignee) {
- case 'me':
- $this->query->eq(Task::TABLE.'.owner_id', $this->userSession->getId());
- break;
- case 'nobody':
- $this->query->eq(Task::TABLE.'.owner_id', 0);
- break;
- default:
- $this->query->ilike(User::TABLE.'.username', '%'.$assignee.'%');
- $this->query->ilike(User::TABLE.'.name', '%'.$assignee.'%');
- }
- }
-
- $this->filterBySubtaskAssignee($values);
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by subtask assignee names
- *
- * @access public
- * @param array $values List of assignees
- * @return TaskFilter
- */
- public function filterBySubtaskAssignee(array $values)
- {
- $subtaskQuery = $this->createSubtaskQuery();
- $subtaskQuery->beginOr();
-
- foreach ($values as $assignee) {
- if ($assignee === 'me') {
- $subtaskQuery->eq(Subtask::TABLE.'.user_id', $this->userSession->getId());
- } else {
- $subtaskQuery->ilike(User::TABLE.'.username', '%'.$assignee.'%');
- $subtaskQuery->ilike(User::TABLE.'.name', '%'.$assignee.'%');
- }
- }
-
- $subtaskQuery->closeOr();
-
- $this->query->in(Task::TABLE.'.id', $subtaskQuery->findAllByColumn('task_id'));
-
- return $this;
- }
-
- /**
- * Filter by color
- *
- * @access public
- * @param string $color_id
- * @return TaskFilter
- */
- public function filterByColor($color_id)
- {
- if ($color_id !== '') {
- $this->query->eq(Task::TABLE.'.color_id', $color_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by colors
- *
- * @access public
- * @param array $colors
- * @return TaskFilter
- */
- public function filterByColors(array $colors)
- {
- $this->query->beginOr();
-
- foreach ($colors as $color) {
- $this->filterByColor($this->color->find($color));
- }
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by column
- *
- * @access public
- * @param integer $column_id
- * @return TaskFilter
- */
- public function filterByColumn($column_id)
- {
- if ($column_id >= 0) {
- $this->query->eq(Task::TABLE.'.column_id', $column_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by column name
- *
- * @access public
- * @param array $values List of column name
- * @return TaskFilter
- */
- public function filterByColumnName(array $values)
- {
- $this->query->beginOr();
-
- foreach ($values as $project) {
- $this->query->ilike(Column::TABLE.'.title', $project);
- }
-
- $this->query->closeOr();
- }
-
- /**
- * Filter by swimlane
- *
- * @access public
- * @param integer $swimlane_id
- * @return TaskFilter
- */
- public function filterBySwimlane($swimlane_id)
- {
- if ($swimlane_id >= 0) {
- $this->query->eq(Task::TABLE.'.swimlane_id', $swimlane_id);
- }
-
- return $this;
- }
-
- /**
- * Filter by status name
- *
- * @access public
- * @param string $status
- * @return TaskFilter
- */
- public function filterByStatusName($status)
- {
- if ($status === 'open' || $status === 'closed') {
- $this->filterByStatus($status === 'open' ? Task::STATUS_OPEN : Task::STATUS_CLOSED);
- }
-
- return $this;
- }
-
- /**
- * Filter by status
- *
- * @access public
- * @param integer $is_active
- * @return TaskFilter
- */
- public function filterByStatus($is_active)
- {
- if ($is_active >= 0) {
- $this->query->eq(Task::TABLE.'.is_active', $is_active);
- }
-
- return $this;
- }
-
- /**
- * Filter by link
- *
- * @access public
- * @param array $values List of links
- * @return TaskFilter
- */
- public function filterByLinkName(array $values)
- {
- $this->query->beginOr();
-
- $link_query = $this->createLinkQuery()->in(Link::TABLE.'.label', $values);
- $matching_task_ids = $link_query->findAllByColumn('task_id');
- if (empty($matching_task_ids)) {
- $this->query->eq(Task::TABLE.'.id', 0);
- } else {
- $this->query->in(Task::TABLE.'.id', $matching_task_ids);
- }
-
- $this->query->closeOr();
-
- return $this;
- }
-
- /**
- * Filter by due date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByDueDate($date)
- {
- $this->query->neq(Task::TABLE.'.date_due', 0);
- $this->query->notNull(Task::TABLE.'.date_due');
- return $this->filterWithOperator(Task::TABLE.'.date_due', $date, true);
- }
-
- /**
- * Filter by due date (range)
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByDueDateRange($start, $end)
- {
- $this->query->gte('date_due', $this->dateParser->getTimestampFromIsoFormat($start));
- $this->query->lte('date_due', $this->dateParser->getTimestampFromIsoFormat($end));
-
- return $this;
- }
-
- /**
- * Filter by start date (range)
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByStartDateRange($start, $end)
- {
- $this->query->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'date_started',
- 'date_completed'
- ));
-
- return $this;
- }
-
- /**
- * Filter by creation date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByCreationDate($date)
- {
- if ($date === 'recently') {
- return $this->filterRecentlyDate(Task::TABLE.'.date_creation');
- }
-
- return $this->filterWithOperator(Task::TABLE.'.date_creation', $date, true);
- }
-
- /**
- * Filter by creation date
- *
- * @access public
- * @param string $start
- * @param string $end
- * @return TaskFilter
- */
- public function filterByCreationDateRange($start, $end)
- {
- $this->query->addCondition($this->getCalendarCondition(
- $this->dateParser->getTimestampFromIsoFormat($start),
- $this->dateParser->getTimestampFromIsoFormat($end),
- 'date_creation',
- 'date_completed'
- ));
-
- return $this;
- }
-
- /**
- * Filter by modification date
- *
- * @access public
- * @param string $date ISO8601 date format
- * @return TaskFilter
- */
- public function filterByModificationDate($date)
- {
- if ($date === 'recently') {
- return $this->filterRecentlyDate(Task::TABLE.'.date_modification');
- }
-
- return $this->filterWithOperator(Task::TABLE.'.date_modification', $date, true);
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @return array
- */
- public function findAll()
- {
- return $this->query->asc(Task::TABLE.'.id')->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Get swimlanes and tasks to display the board
- *
- * @access public
- * @return array
- */
- public function getBoard($project_id)
- {
- $tasks = $this->filterByProject($project_id)->query->asc(Task::TABLE.'.position')->findAll();
-
- return $this->board->getBoard($project_id, function ($project_id, $column_id, $swimlane_id) use ($tasks) {
- return array_filter($tasks, function (array $task) use ($column_id, $swimlane_id) {
- return $task['column_id'] == $column_id && $task['swimlane_id'] == $swimlane_id;
- });
- });
- }
-
- /**
- * Filter with an operator
- *
- * @access public
- * @param string $field
- * @param string $value
- * @param boolean $is_date
- * @return TaskFilter
- */
- private function filterWithOperator($field, $value, $is_date)
- {
- $operators = array(
- '<=' => 'lte',
- '>=' => 'gte',
- '<' => 'lt',
- '>' => 'gt',
- );
-
- foreach ($operators as $operator => $method) {
- if (strpos($value, $operator) === 0) {
- $value = substr($value, strlen($operator));
- $this->query->$method($field, $is_date ? $this->dateParser->getTimestampFromIsoFormat($value) : $value);
- return $this;
- }
- }
-
- if ($is_date) {
- $timestamp = $this->dateParser->getTimestampFromIsoFormat($value);
- $this->query->gte($field, $timestamp);
- $this->query->lte($field, $timestamp + 86399);
- } else {
- $this->query->eq($field, $value);
- }
-
- return $this;
- }
-
- /**
- * Use the board_highlight_period for the "recently" keyword
- *
- * @access private
- * @param string $field
- * @return TaskFilter
- */
- private function filterRecentlyDate($field)
- {
- $duration = $this->config->get('board_highlight_period', 0);
-
- if ($duration > 0) {
- $this->query->gte($field, time() - $duration);
- }
-
- return $this;
- }
-}
diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php
index 7bca2284..1840b505 100644
--- a/app/Model/TaskFinder.php
+++ b/app/Model/TaskFinder.php
@@ -363,6 +363,27 @@ class TaskFinder extends Base
}
/**
+ * Get iCal query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getICalQuery()
+ {
+ return $this->db->table(Task::TABLE)
+ ->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id')
+ ->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id')
+ ->columns(
+ Task::TABLE.'.*',
+ 'ua.email AS assignee_email',
+ 'ua.name AS assignee_name',
+ 'ua.username AS assignee_username',
+ 'uc.email AS creator_email',
+ 'uc.username AS creator_username'
+ );
+ }
+
+ /**
* Count all tasks for a given project and status
*
* @access public
diff --git a/app/Model/UserFilter.php b/app/Model/UserFilter.php
deleted file mode 100644
index ff546e96..00000000
--- a/app/Model/UserFilter.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-
-namespace Kanboard\Model;
-
-/**
- * User Filter
- *
- * @package model
- * @author Frederic Guillot
- */
-class UserFilter extends Base
-{
- /**
- * Search query
- *
- * @access private
- * @var string
- */
- private $input;
-
- /**
- * Query
- *
- * @access protected
- * @var \PicoDb\Table
- */
- protected $query;
-
- /**
- * Initialize filter
- *
- * @access public
- * @param string $input
- * @return UserFilter
- */
- public function create($input)
- {
- $this->query = $this->db->table(User::TABLE);
- $this->input = $input;
- return $this;
- }
-
- /**
- * Filter users by name or username
- *
- * @access public
- * @return UserFilter
- */
- public function filterByUsernameOrByName()
- {
- $this->query->beginOr()
- ->ilike('username', '%'.$this->input.'%')
- ->ilike('name', '%'.$this->input.'%')
- ->closeOr();
-
- return $this;
- }
-
- /**
- * Get all results of the filter
- *
- * @access public
- * @return array
- */
- public function findAll()
- {
- return $this->query->findAll();
- }
-
- /**
- * Get the PicoDb query
- *
- * @access public
- * @return \PicoDb\Table
- */
- public function getQuery()
- {
- return $this->query;
- }
-}
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 3e654a4e..18c1d578 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -49,9 +49,7 @@ class ClassProvider implements ServiceProviderInterface
'ProjectNotification',
'ProjectMetadata',
'ProjectGroupRole',
- 'ProjectGroupRoleFilter',
'ProjectUserRole',
- 'ProjectUserRoleFilter',
'RememberMeSession',
'Subtask',
'SubtaskTimeTracking',
@@ -63,7 +61,6 @@ class ClassProvider implements ServiceProviderInterface
'TaskExternalLink',
'TaskFinder',
'TaskFile',
- 'TaskFilter',
'TaskLink',
'TaskModification',
'TaskPermission',
@@ -79,15 +76,6 @@ class ClassProvider implements ServiceProviderInterface
'UserUnreadNotification',
'UserMetadata',
),
- 'Formatter' => array(
- 'TaskFilterGanttFormatter',
- 'TaskFilterAutoCompleteFormatter',
- 'TaskFilterCalendarFormatter',
- 'TaskFilterICalendarFormatter',
- 'ProjectGanttFormatter',
- 'UserFilterAutoCompleteFormatter',
- 'GroupAutoCompleteFormatter',
- ),
'Validator' => array(
'ActionValidator',
'AuthValidator',
diff --git a/app/ServiceProvider/FilterProvider.php b/app/ServiceProvider/FilterProvider.php
new file mode 100644
index 00000000..555cb262
--- /dev/null
+++ b/app/ServiceProvider/FilterProvider.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Kanboard\ServiceProvider;
+
+use Kanboard\Core\Filter\LexerBuilder;
+use Kanboard\Core\Filter\QueryBuilder;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskCategoryFilter;
+use Kanboard\Filter\TaskColorFilter;
+use Kanboard\Filter\TaskColumnFilter;
+use Kanboard\Filter\TaskCreationDateFilter;
+use Kanboard\Filter\TaskDescriptionFilter;
+use Kanboard\Filter\TaskDueDateFilter;
+use Kanboard\Filter\TaskIdFilter;
+use Kanboard\Filter\TaskLinkFilter;
+use Kanboard\Filter\TaskModificationDateFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskReferenceFilter;
+use Kanboard\Filter\TaskStatusFilter;
+use Kanboard\Filter\TaskSubtaskAssigneeFilter;
+use Kanboard\Filter\TaskSwimlaneFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\ProjectGroupRole;
+use Kanboard\Model\ProjectUserRole;
+use Kanboard\Model\User;
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Filter Provider
+ *
+ * @package serviceProvider
+ * @author Frederic Guillot
+ */
+class FilterProvider implements ServiceProviderInterface
+{
+ /**
+ * Register providers
+ *
+ * @access public
+ * @param \Pimple\Container $container
+ * @return \Pimple\Container
+ */
+ public function register(Container $container)
+ {
+ $container['projectGroupRoleQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(ProjectGroupRole::TABLE));
+ return $builder;
+ });
+
+ $container['projectUserRoleQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(ProjectUserRole::TABLE));
+ return $builder;
+ });
+
+ $container['userQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(User::TABLE));
+ return $builder;
+ });
+
+ $container['projectQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['db']->table(Project::TABLE));
+ return $builder;
+ });
+
+ $container['taskQuery'] = $container->factory(function ($c) {
+ $builder = new QueryBuilder();
+ $builder->withQuery($c['taskFinder']->getExtendedQuery());
+ return $builder;
+ });
+
+ $container['taskLexer'] = $container->factory(function ($c) {
+ $builder = new LexerBuilder();
+
+ $builder
+ ->withQuery($c['taskFinder']->getExtendedQuery())
+ ->withFilter(TaskAssigneeFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ )
+ ->withFilter(new TaskCategoryFilter())
+ ->withFilter(TaskColorFilter::getInstance()->setColorModel($c['color']))
+ ->withFilter(new TaskColumnFilter())
+ ->withFilter(new TaskCreationDateFilter())
+ ->withFilter(new TaskDescriptionFilter())
+ ->withFilter(new TaskDueDateFilter())
+ ->withFilter(new TaskIdFilter())
+ ->withFilter(TaskLinkFilter::getInstance()
+ ->setDatabase($c['db'])
+ )
+ ->withFilter(new TaskModificationDateFilter())
+ ->withFilter(new TaskProjectFilter())
+ ->withFilter(new TaskReferenceFilter())
+ ->withFilter(new TaskStatusFilter())
+ ->withFilter(TaskSubtaskAssigneeFilter::getInstance()
+ ->setCurrentUserId($c['userSession']->getId())
+ ->setDatabase($c['db'])
+ )
+ ->withFilter(new TaskSwimlaneFilter())
+ ->withFilter(new TaskTitleFilter(), true)
+ ;
+
+ return $builder;
+ });
+
+ return $container;
+ }
+}
diff --git a/app/ServiceProvider/HelperProvider.php b/app/ServiceProvider/HelperProvider.php
index 43a78e32..3590afa5 100644
--- a/app/ServiceProvider/HelperProvider.php
+++ b/app/ServiceProvider/HelperProvider.php
@@ -13,12 +13,14 @@ class HelperProvider implements ServiceProviderInterface
{
$container['helper'] = new Helper($container);
$container['helper']->register('app', '\Kanboard\Helper\AppHelper');
+ $container['helper']->register('calendar', '\Kanboard\Helper\CalendarHelper');
$container['helper']->register('asset', '\Kanboard\Helper\AssetHelper');
$container['helper']->register('board', '\Kanboard\Helper\BoardHelper');
$container['helper']->register('dt', '\Kanboard\Helper\DateHelper');
$container['helper']->register('file', '\Kanboard\Helper\FileHelper');
$container['helper']->register('form', '\Kanboard\Helper\FormHelper');
$container['helper']->register('hook', '\Kanboard\Helper\HookHelper');
+ $container['helper']->register('ical', '\Kanboard\Helper\ICalHelper');
$container['helper']->register('layout', '\Kanboard\Helper\LayoutHelper');
$container['helper']->register('model', '\Kanboard\Helper\ModelHelper');
$container['helper']->register('subtask', '\Kanboard\Helper\SubtaskHelper');
diff --git a/app/common.php b/app/common.php
index 7dbd7587..da624844 100644
--- a/app/common.php
+++ b/app/common.php
@@ -39,4 +39,5 @@ $container->register(new Kanboard\ServiceProvider\RouteProvider);
$container->register(new Kanboard\ServiceProvider\ActionProvider);
$container->register(new Kanboard\ServiceProvider\ExternalLinkProvider);
$container->register(new Kanboard\ServiceProvider\AvatarProvider);
+$container->register(new Kanboard\ServiceProvider\FilterProvider);
$container->register(new Kanboard\ServiceProvider\PluginProvider);
diff --git a/composer.lock b/composer.lock
index 438118a2..70881a39 100644
--- a/composer.lock
+++ b/composer.lock
@@ -9,16 +9,16 @@
"packages": [
{
"name": "christian-riesen/base32",
- "version": "1.2.2",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/ChristianRiesen/base32.git",
- "reference": "fbe67d49d45dc789f942ef828c787550ebb894bc"
+ "reference": "fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/fbe67d49d45dc789f942ef828c787550ebb894bc",
- "reference": "fbe67d49d45dc789f942ef828c787550ebb894bc",
+ "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4",
+ "reference": "fde061a370b0a97fdcd33d9d5f7b1b70ce1f79d4",
"shasum": ""
},
"require": {
@@ -59,7 +59,7 @@
"encode",
"rfc4648"
],
- "time": "2015-09-27 23:45:02"
+ "time": "2016-04-07 07:45:31"
},
{
"name": "christian-riesen/otp",
@@ -397,16 +397,16 @@
},
{
"name": "paragonie/random_compat",
- "version": "v2.0.1",
+ "version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
- "reference": "76e90f747b769b347fe584e8015a014549107d35"
+ "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/random_compat/zipball/76e90f747b769b347fe584e8015a014549107d35",
- "reference": "76e90f747b769b347fe584e8015a014549107d35",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/088c04e2f261c33bed6ca5245491cfca69195ccf",
+ "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf",
"shasum": ""
},
"require": {
@@ -441,7 +441,7 @@
"pseudorandom",
"random"
],
- "time": "2016-03-18 20:36:13"
+ "time": "2016-04-03 06:00:07"
},
{
"name": "pimple/pimple",
diff --git a/doc/installation.markdown b/doc/installation.markdown
index dd4283f8..c796ac65 100644
--- a/doc/installation.markdown
+++ b/doc/installation.markdown
@@ -29,7 +29,7 @@ From the repository (development version)
You must install [composer](https://getcomposer.org/) to use this method.
1. `git clone https://github.com/fguillot/kanboard.git`
-2. `composer install`
+2. `composer install --no-dev`
3. Go to the third step just above
Note: This method will install the **current development version**, use at your own risk.
diff --git a/doc/plugin-hooks.markdown b/doc/plugin-hooks.markdown
index 5dc56cd1..a00aba16 100644
--- a/doc/plugin-hooks.markdown
+++ b/doc/plugin-hooks.markdown
@@ -28,15 +28,6 @@ Some hooks can have only one listener:
- `$start` (DateTime)
- `$end` (DateTime)
-#### model:subtask-time-tracking:calendar:events
-
-- Override subtask time tracking events to display the calendar
-- Arguments:
- - `$user_id` (integer)
- - `$events` (array)
- - `$start` (string, ISO-8601 format)
- - `$end` (string, ISO-8601 format)
-
### Merge hooks
"Merge hooks" act in the same way as the function `array_merge`. The hook callback must return an array. This array will be merged with the default one.
diff --git a/doc/update.markdown b/doc/update.markdown
index 7be8a65a..12ac152d 100644
--- a/doc/update.markdown
+++ b/doc/update.markdown
@@ -27,7 +27,7 @@ From the repository (development version)
-----------------------------------------
1. `git pull`
-2. `composer install`
+2. `composer install --no-dev`
3. Login and check if everything is ok
Note: This method will install the **current development version**, use at your own risk.
diff --git a/tests/units/Base.php b/tests/units/Base.php
index 563035f6..5125ffb9 100644
--- a/tests/units/Base.php
+++ b/tests/units/Base.php
@@ -40,6 +40,7 @@ abstract class Base extends PHPUnit_Framework_TestCase
$this->container->register(new Kanboard\ServiceProvider\NotificationProvider);
$this->container->register(new Kanboard\ServiceProvider\RouteProvider);
$this->container->register(new Kanboard\ServiceProvider\AvatarProvider);
+ $this->container->register(new Kanboard\ServiceProvider\FilterProvider);
$this->container['dispatcher'] = new TraceableEventDispatcher(
new EventDispatcher,
diff --git a/tests/units/Core/Filter/LexerBuilderTest.php b/tests/units/Core/Filter/LexerBuilderTest.php
new file mode 100644
index 00000000..ac5315bb
--- /dev/null
+++ b/tests/units/Core/Filter/LexerBuilderTest.php
@@ -0,0 +1,106 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Filter\LexerBuilder;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+
+class LexerBuilderTest extends Base
+{
+ public function testBuilderThatReturnResult()
+ {
+ $project = new Project($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $project->create(array('name' => 'Project')));
+ $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'Test')));
+
+ $builder = new LexerBuilder();
+ $builder->withFilter(new TaskAssigneeFilter());
+ $builder->withFilter(new TaskTitleFilter(), true);
+ $builder->withQuery($query);
+ $tasks = $builder->build('assignee:nobody')->toArray();
+
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Test', $tasks[0]['title']);
+ }
+
+ public function testBuilderThatReturnNothing()
+ {
+ $project = new Project($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $project->create(array('name' => 'Project')));
+ $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'Test')));
+
+ $builder = new LexerBuilder();
+ $builder->withFilter(new TaskAssigneeFilter());
+ $builder->withFilter(new TaskTitleFilter(), true);
+ $builder->withQuery($query);
+ $tasks = $builder->build('something')->toArray();
+
+ $this->assertCount(0, $tasks);
+ }
+
+ public function testBuilderWithEmptyInput()
+ {
+ $project = new Project($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $project->create(array('name' => 'Project')));
+ $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'Test')));
+
+ $builder = new LexerBuilder();
+ $builder->withFilter(new TaskAssigneeFilter());
+ $builder->withFilter(new TaskTitleFilter(), true);
+ $builder->withQuery($query);
+ $tasks = $builder->build('')->toArray();
+
+ $this->assertCount(1, $tasks);
+ }
+
+ public function testBuilderWithMultipleMatches()
+ {
+ $project = new Project($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $taskFinder = new TaskFinder($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $project->create(array('name' => 'Project')));
+ $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'ABC', 'owner_id' => 1)));
+ $this->assertNotFalse($taskCreation->create(array('project_id' => 1, 'title' => 'DEF')));
+
+ $builder = new LexerBuilder();
+ $builder->withFilter(new TaskAssigneeFilter());
+ $builder->withFilter(new TaskTitleFilter(), true);
+ $builder->withQuery($query);
+ $tasks = $builder->build('assignee:nobody assignee:1')->toArray();
+
+ $this->assertCount(2, $tasks);
+ }
+
+ public function testClone()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $builder = new LexerBuilder();
+ $builder->withFilter(new TaskAssigneeFilter());
+ $builder->withFilter(new TaskTitleFilter());
+ $builder->withQuery($query);
+
+ $clone = clone($builder);
+ $this->assertFalse($builder === $clone);
+ $this->assertFalse($builder->build('test')->getQuery() === $clone->build('test')->getQuery());
+ }
+}
diff --git a/tests/units/Core/Filter/LexerTest.php b/tests/units/Core/Filter/LexerTest.php
new file mode 100644
index 00000000..3f3e368e
--- /dev/null
+++ b/tests/units/Core/Filter/LexerTest.php
@@ -0,0 +1,100 @@
+<?php
+
+require_once __DIR__.'/../../Base.php';
+
+use Kanboard\Core\Filter\Lexer;
+
+class LexerTest extends Base
+{
+ public function testTokenizeWithNoDefaultToken()
+ {
+ $lexer = new Lexer();
+ $this->assertSame(array(), $lexer->tokenize('This is Kanboard'));
+ }
+
+ public function testTokenizeWithDefaultToken()
+ {
+ $lexer = new Lexer();
+ $lexer->setDefaultToken('myDefaultToken');
+
+ $expected = array(
+ 'myDefaultToken' => array('This is Kanboard'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('This is Kanboard'));
+ }
+
+ public function testTokenizeWithCustomToken()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(assignee:)/", 'T_USER');
+
+ $expected = array(
+ 'T_USER' => array('admin'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('assignee:admin something else'));
+ }
+
+ public function testTokenizeWithCustomTokenAndDefaultToken()
+ {
+ $lexer = new Lexer();
+ $lexer->setDefaultToken('myDefaultToken');
+ $lexer->addToken("/^(assignee:)/", 'T_USER');
+
+ $expected = array(
+ 'T_USER' => array('admin'),
+ 'myDefaultToken' => array('something else'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('assignee:admin something else'));
+ }
+
+ public function testTokenizeWithQuotedString()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(assignee:)/", 'T_USER');
+
+ $expected = array(
+ 'T_USER' => array('Foo Bar'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('assignee:"Foo Bar" something else'));
+ }
+
+ public function testTokenizeWithNumber()
+ {
+ $lexer = new Lexer();
+ $lexer->setDefaultToken('myDefaultToken');
+
+ $expected = array(
+ 'myDefaultToken' => array('#123'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('#123'));
+ }
+
+ public function testTokenizeWithStringDate()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(date:)/", 'T_DATE');
+
+ $expected = array(
+ 'T_DATE' => array('today'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('date:today something else'));
+ }
+
+ public function testTokenizeWithIsoDate()
+ {
+ $lexer = new Lexer();
+ $lexer->addToken("/^(date:)/", 'T_DATE');
+
+ $expected = array(
+ 'T_DATE' => array('<=2016-01-01'),
+ );
+
+ $this->assertSame($expected, $lexer->tokenize('date:<=2016-01-01 something else'));
+ }
+}
diff --git a/tests/units/Core/Filter/OrCriteriaTest.php b/tests/units/Core/Filter/OrCriteriaTest.php
new file mode 100644
index 00000000..787d3461
--- /dev/null
+++ b/tests/units/Core/Filter/OrCriteriaTest.php
@@ -0,0 +1,58 @@
+<?php
+
+use Kanboard\Core\Filter\OrCriteria;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskTitleFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\User;
+
+require_once __DIR__.'/../../Base.php';
+
+class OrCriteriaTest extends Base
+{
+ public function testWithSameFilter()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $userModel = new User($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 2)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $criteria = new OrCriteria();
+ $criteria->withQuery($query);
+ $criteria->withFilter(TaskAssigneeFilter::getInstance(1));
+ $criteria->withFilter(TaskAssigneeFilter::getInstance(2));
+ $criteria->apply();
+
+ $this->assertCount(2, $query->findAll());
+ }
+
+ public function testWithDifferentFilter()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $userModel = new User($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'ABC', 'project_id' => 1, 'owner_id' => 2)));
+ $this->assertEquals(2, $taskCreation->create(array('title' => 'DEF', 'project_id' => 1, 'owner_id' => 1)));
+
+ $criteria = new OrCriteria();
+ $criteria->withQuery($query);
+ $criteria->withFilter(TaskAssigneeFilter::getInstance(1));
+ $criteria->withFilter(TaskTitleFilter::getInstance('ABC'));
+ $criteria->apply();
+
+ $this->assertCount(2, $query->findAll());
+ }
+}
diff --git a/tests/units/Core/LexerTest.php b/tests/units/Core/LexerTest.php
deleted file mode 100644
index 55370aab..00000000
--- a/tests/units/Core/LexerTest.php
+++ /dev/null
@@ -1,468 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Core\Lexer;
-
-class LexerTest extends Base
-{
- public function testSwimlaneQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'swimlane:', 'token' => 'T_SWIMLANE'), array('match' => 'Version 42', 'token' => 'T_STRING')),
- $lexer->tokenize('swimlane:"Version 42"')
- );
-
- $this->assertEquals(
- array(array('match' => 'swimlane:', 'token' => 'T_SWIMLANE'), array('match' => 'v3', 'token' => 'T_STRING')),
- $lexer->tokenize('swimlane:v3')
- );
-
- $this->assertEquals(
- array('T_SWIMLANE' => array('v3')),
- $lexer->map($lexer->tokenize('swimlane:v3'))
- );
-
- $this->assertEquals(
- array('T_SWIMLANE' => array('Version 42', 'v3')),
- $lexer->map($lexer->tokenize('swimlane:"Version 42" swimlane:v3'))
- );
- }
-
- public function testAssigneeQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'me', 'token' => 'T_STRING')),
- $lexer->tokenize('assignee:me')
- );
-
- $this->assertEquals(
- array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'everybody', 'token' => 'T_STRING')),
- $lexer->tokenize('assignee:everybody')
- );
-
- $this->assertEquals(
- array(array('match' => 'assignee:', 'token' => 'T_ASSIGNEE'), array('match' => 'nobody', 'token' => 'T_STRING')),
- $lexer->tokenize('assignee:nobody')
- );
-
- $this->assertEquals(
- array('T_ASSIGNEE' => array('nobody')),
- $lexer->map($lexer->tokenize('assignee:nobody'))
- );
-
- $this->assertEquals(
- array('T_ASSIGNEE' => array('John Doe', 'me')),
- $lexer->map($lexer->tokenize('assignee:"John Doe" assignee:me'))
- );
- }
-
- public function testColorQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'color:', 'token' => 'T_COLOR'), array('match' => 'Blue', 'token' => 'T_STRING')),
- $lexer->tokenize('color:Blue')
- );
-
- $this->assertEquals(
- array(array('match' => 'color:', 'token' => 'T_COLOR'), array('match' => 'Dark Grey', 'token' => 'T_STRING')),
- $lexer->tokenize('color:"Dark Grey"')
- );
-
- $this->assertEquals(
- array('T_COLOR' => array('Blue')),
- $lexer->map($lexer->tokenize('color:Blue'))
- );
-
- $this->assertEquals(
- array('T_COLOR' => array('Dark Grey')),
- $lexer->map($lexer->tokenize('color:"Dark Grey"'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('color: '))
- );
- }
-
- public function testCategoryQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'category:', 'token' => 'T_CATEGORY'), array('match' => 'Feature Request', 'token' => 'T_STRING')),
- $lexer->tokenize('category:"Feature Request"')
- );
-
- $this->assertEquals(
- array('T_CATEGORY' => array('Feature Request')),
- $lexer->map($lexer->tokenize('category:"Feature Request"'))
- );
-
- $this->assertEquals(
- array('T_CATEGORY' => array('Feature Request', 'Bug')),
- $lexer->map($lexer->tokenize('category:"Feature Request" category:Bug'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('category: '))
- );
- }
-
- public function testLinkQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'link:', 'token' => 'T_LINK'), array('match' => 'is a milestone of', 'token' => 'T_STRING')),
- $lexer->tokenize('link:"is a milestone of"')
- );
-
- $this->assertEquals(
- array('T_LINK' => array('is a milestone of')),
- $lexer->map($lexer->tokenize('link:"is a milestone of"'))
- );
-
- $this->assertEquals(
- array('T_LINK' => array('is a milestone of', 'fixes')),
- $lexer->map($lexer->tokenize('link:"is a milestone of" link:fixes'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('link: '))
- );
- }
-
- public function testColumnQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'column:', 'token' => 'T_COLUMN'), array('match' => 'Feature Request', 'token' => 'T_STRING')),
- $lexer->tokenize('column:"Feature Request"')
- );
-
- $this->assertEquals(
- array('T_COLUMN' => array('Feature Request')),
- $lexer->map($lexer->tokenize('column:"Feature Request"'))
- );
-
- $this->assertEquals(
- array('T_COLUMN' => array('Feature Request', 'Bug')),
- $lexer->map($lexer->tokenize('column:"Feature Request" column:Bug'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('column: '))
- );
- }
-
- public function testProjectQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'project:', 'token' => 'T_PROJECT'), array('match' => 'My project', 'token' => 'T_STRING')),
- $lexer->tokenize('project:"My project"')
- );
-
- $this->assertEquals(
- array('T_PROJECT' => array('My project')),
- $lexer->map($lexer->tokenize('project:"My project"'))
- );
-
- $this->assertEquals(
- array('T_PROJECT' => array('My project', 'plop')),
- $lexer->map($lexer->tokenize('project:"My project" project:plop'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('project: '))
- );
- }
-
- public function testStatusQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'status:', 'token' => 'T_STATUS'), array('match' => 'open', 'token' => 'T_STRING')),
- $lexer->tokenize('status:open')
- );
-
- $this->assertEquals(
- array(array('match' => 'status:', 'token' => 'T_STATUS'), array('match' => 'closed', 'token' => 'T_STRING')),
- $lexer->tokenize('status:closed')
- );
-
- $this->assertEquals(
- array('T_STATUS' => 'open'),
- $lexer->map($lexer->tokenize('status:open'))
- );
-
- $this->assertEquals(
- array('T_STATUS' => 'closed'),
- $lexer->map($lexer->tokenize('status:closed'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('status: '))
- );
- }
-
- public function testReferenceQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'ref:', 'token' => 'T_REFERENCE'), array('match' => '123', 'token' => 'T_STRING')),
- $lexer->tokenize('ref:123')
- );
-
- $this->assertEquals(
- array(array('match' => 'reference:', 'token' => 'T_REFERENCE'), array('match' => '456', 'token' => 'T_STRING')),
- $lexer->tokenize('reference:456')
- );
-
- $this->assertEquals(
- array('T_REFERENCE' => '123'),
- $lexer->map($lexer->tokenize('reference:123'))
- );
-
- $this->assertEquals(
- array('T_REFERENCE' => '456'),
- $lexer->map($lexer->tokenize('ref:456'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('ref: '))
- );
- }
-
- public function testDescriptionQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'description:', 'token' => 'T_DESCRIPTION'), array('match' => 'my text search', 'token' => 'T_STRING')),
- $lexer->tokenize('description:"my text search"')
- );
-
- $this->assertEquals(
- array('T_DESCRIPTION' => 'my text search'),
- $lexer->map($lexer->tokenize('description:"my text search"'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('description: '))
- );
- }
-
- public function testDueDateQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '<2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:<2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '>2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:>2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '<=2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:<=2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => '>=2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('due:>=2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => 'yesterday', 'token' => 'T_DATE')),
- $lexer->tokenize('due:yesterday')
- );
-
- $this->assertEquals(
- array(array('match' => 'due:', 'token' => 'T_DUE'), array('match' => 'tomorrow', 'token' => 'T_DATE')),
- $lexer->tokenize('due:tomorrow')
- );
-
- $this->assertEquals(
- array(),
- $lexer->tokenize('due:#2015-05-01')
- );
-
- $this->assertEquals(
- array(),
- $lexer->tokenize('due:01-05-1024')
- );
-
- $this->assertEquals(
- array('T_DUE' => '2015-05-01'),
- $lexer->map($lexer->tokenize('due:2015-05-01'))
- );
-
- $this->assertEquals(
- array('T_DUE' => '<2015-05-01'),
- $lexer->map($lexer->tokenize('due:<2015-05-01'))
- );
-
- $this->assertEquals(
- array('T_DUE' => 'today'),
- $lexer->map($lexer->tokenize('due:today'))
- );
- }
-
- public function testModifiedQuery()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('modified:2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '<2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('modified:<2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'modified:', 'token' => 'T_UPDATED'), array('match' => '>2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('modified:>2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => '<=2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('updated:<=2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => '>=2015-05-01', 'token' => 'T_DATE')),
- $lexer->tokenize('updated:>=2015-05-01')
- );
-
- $this->assertEquals(
- array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => 'yesterday', 'token' => 'T_DATE')),
- $lexer->tokenize('updated:yesterday')
- );
-
- $this->assertEquals(
- array(array('match' => 'updated:', 'token' => 'T_UPDATED'), array('match' => 'tomorrow', 'token' => 'T_DATE')),
- $lexer->tokenize('updated:tomorrow')
- );
-
- $this->assertEquals(
- array(),
- $lexer->tokenize('updated:#2015-05-01')
- );
-
- $this->assertEquals(
- array(),
- $lexer->tokenize('modified:01-05-1024')
- );
-
- $this->assertEquals(
- array('T_UPDATED' => '2015-05-01'),
- $lexer->map($lexer->tokenize('modified:2015-05-01'))
- );
-
- $this->assertEquals(
- array('T_UPDATED' => '<2015-05-01'),
- $lexer->map($lexer->tokenize('modified:<2015-05-01'))
- );
-
- $this->assertEquals(
- array('T_UPDATED' => 'today'),
- $lexer->map($lexer->tokenize('modified:today'))
- );
- }
-
- public function testMultipleCriterias()
- {
- $lexer = new Lexer;
-
- $this->assertEquals(
- array('T_COLOR' => array('Dark Grey'), 'T_ASSIGNEE' => array('Fred G'), 'T_TITLE' => 'my task title'),
- $lexer->map($lexer->tokenize('color:"Dark Grey" assignee:"Fred G" my task title'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow')),
- $lexer->map($lexer->tokenize('my title color:yellow'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title', 'T_DUE' => '2015-04-01'),
- $lexer->map($lexer->tokenize('my title due:2015-04-01'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'awesome', 'T_DUE' => '<=2015-04-01'),
- $lexer->map($lexer->tokenize('due:<=2015-04-01 awesome'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'awesome', 'T_DUE' => 'today'),
- $lexer->map($lexer->tokenize('due:today awesome'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow'), 'T_DUE' => '2015-04-01'),
- $lexer->map($lexer->tokenize('my title color:yellow due:2015-04-01'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title', 'T_COLOR' => array('yellow'), 'T_DUE' => '2015-04-01', 'T_ASSIGNEE' => array('John Doe')),
- $lexer->map($lexer->tokenize('my title color:yellow due:2015-04-01 assignee:"John Doe"'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title'),
- $lexer->map($lexer->tokenize('my title color:'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title'),
- $lexer->map($lexer->tokenize('my title color:assignee:'))
- );
-
- $this->assertEquals(
- array('T_TITLE' => 'my title'),
- $lexer->map($lexer->tokenize('my title '))
- );
-
- $this->assertEquals(
- array('T_TITLE' => '#123'),
- $lexer->map($lexer->tokenize('#123'))
- );
-
- $this->assertEquals(
- array(),
- $lexer->map($lexer->tokenize('color:assignee:'))
- );
- }
-}
diff --git a/tests/units/Filter/TaskAssigneeFilterTest.php b/tests/units/Filter/TaskAssigneeFilterTest.php
new file mode 100644
index 00000000..356342c5
--- /dev/null
+++ b/tests/units/Filter/TaskAssigneeFilterTest.php
@@ -0,0 +1,159 @@
+<?php
+
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Model\Project;
+use Kanboard\Model\TaskCreation;
+use Kanboard\Model\TaskFinder;
+use Kanboard\Model\User;
+
+require_once __DIR__.'/../Base.php';
+
+class TaskAssigneeFilterTest extends Base
+{
+ public function testWithIntegerAssigneeId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue(1);
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue(123);
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithStringAssigneeId()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('1');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue("123");
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithUsername()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('admin');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('foobar');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithName()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $userModel = new User($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(2, $userModel->create(array('username' => 'foobar', 'name' => 'Foo Bar')));
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 2)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('foo bar');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('bob');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+
+ public function testWithNobody()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->withQuery($query);
+ $filter->withValue('nobody');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+ }
+
+ public function testWithCurrentUser()
+ {
+ $taskFinder = new TaskFinder($this->container);
+ $taskCreation = new TaskCreation($this->container);
+ $projectModel = new Project($this->container);
+ $query = $taskFinder->getExtendedQuery();
+
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Test')));
+ $this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1, 'owner_id' => 1)));
+
+ $filter = new TaskAssigneeFilter();
+ $filter->setCurrentUserId(1);
+ $filter->withQuery($query);
+ $filter->withValue('me');
+ $filter->apply();
+
+ $this->assertCount(1, $query->findAll());
+
+ $filter = new TaskAssigneeFilter();
+ $filter->setCurrentUserId(2);
+ $filter->withQuery($query);
+ $filter->withValue('me');
+ $filter->apply();
+
+ $this->assertCount(0, $query->findAll());
+ }
+}
diff --git a/tests/units/Formatter/TaskFilterCalendarFormatterTest.php b/tests/units/Formatter/TaskFilterCalendarFormatterTest.php
deleted file mode 100644
index 09dd0de6..00000000
--- a/tests/units/Formatter/TaskFilterCalendarFormatterTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Formatter\TaskFilterCalendarFormatter;
-
-class TaskFilterCalendarFormatterTest extends Base
-{
- public function testCopy()
- {
- $tf = new TaskFilterCalendarFormatter($this->container);
- $filter1 = $tf->create()->setFullDay();
- $filter2 = $tf->copy();
-
- $this->assertTrue($filter1 !== $filter2);
- $this->assertTrue($filter1->query !== $filter2->query);
- $this->assertTrue($filter1->query->condition !== $filter2->query->condition);
- $this->assertTrue($filter1->isFullDay());
- $this->assertFalse($filter2->isFullDay());
- }
-}
diff --git a/tests/units/Formatter/TaskFilterGanttFormatterTest.php b/tests/units/Formatter/TaskFilterGanttFormatterTest.php
deleted file mode 100644
index 14804784..00000000
--- a/tests/units/Formatter/TaskFilterGanttFormatterTest.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Formatter\TaskFilterGanttFormatter;
-use Kanboard\Model\Project;
-use Kanboard\Model\TaskCreation;
-use Kanboard\Core\DateParser;
-
-class TaskFilterGanttFormatterTest extends Base
-{
- public function testFormat()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilterGanttFormatter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
-
- $this->assertNotEmpty($tf->search('status:open')->format());
- }
-}
diff --git a/tests/units/Formatter/TaskFilterICalendarFormatterTest.php b/tests/units/Formatter/TaskFilterICalendarFormatterTest.php
deleted file mode 100644
index 6de9cf0f..00000000
--- a/tests/units/Formatter/TaskFilterICalendarFormatterTest.php
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Eluceo\iCal\Component\Calendar;
-use Kanboard\Formatter\TaskFilterICalendarFormatter;
-use Kanboard\Model\Project;
-use Kanboard\Model\User;
-use Kanboard\Model\TaskCreation;
-use Kanboard\Core\DateParser;
-use Kanboard\Model\Config;
-
-class TaskFilterICalendarFormatterTest extends Base
-{
- public function testIcalEventsWithCreatorAndDueDate()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilterICalendarFormatter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'creator_id' => 1, 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
-
- $ics = $tf->create()
- ->filterByDueDateRange(strtotime('-1 month'), strtotime('+1 month'))
- ->setFullDay()
- ->setCalendar(new Calendar('Kanboard'))
- ->setColumns('date_due')
- ->addFullDayEvents()
- ->format();
-
- $this->assertContains('UID:task-#1-date_due', $ics);
- $this->assertContains('DTSTART;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('-2 days')), $ics);
- $this->assertContains('DTEND;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('-2 days')), $ics);
- $this->assertContains('URL:http://localhost/?controller=task&action=show&task_id=1&project_id=1', $ics);
- $this->assertContains('SUMMARY:#1 task1', $ics);
- $this->assertContains('ATTENDEE:MAILTO:admin@kanboard.local', $ics);
- $this->assertContains('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE', $ics);
- }
-
- public function testIcalEventsWithAssigneeAndDueDate()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilterICalendarFormatter($this->container);
- $u = new User($this->container);
- $c = new Config($this->container);
-
- $this->assertNotFalse($c->save(array('application_url' => 'http://kb/')));
- $this->assertEquals('http://kb/', $c->get('application_url'));
-
- $this->assertNotFalse($u->update(array('id' => 1, 'email' => 'bob@localhost')));
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'owner_id' => 1, 'date_due' => $dp->getTimestampFromIsoFormat('+5 days'))));
-
- $ics = $tf->create()
- ->filterByDueDateRange(strtotime('-1 month'), strtotime('+1 month'))
- ->setFullDay()
- ->setCalendar(new Calendar('Kanboard'))
- ->setColumns('date_due')
- ->addFullDayEvents()
- ->format();
-
- $this->assertContains('UID:task-#1-date_due', $ics);
- $this->assertContains('DTSTART;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('+5 days')), $ics);
- $this->assertContains('DTEND;TZID=UTC;VALUE=DATE:'.date('Ymd', strtotime('+5 days')), $ics);
- $this->assertContains('URL:http://kb/?controller=task&action=show&task_id=1&project_id=1', $ics);
- $this->assertContains('SUMMARY:#1 task1', $ics);
- $this->assertContains('ORGANIZER;CN=admin:MAILTO:bob@localhost', $ics);
- $this->assertContains('X-MICROSOFT-CDO-ALLDAYEVENT:TRUE', $ics);
- }
-}
diff --git a/tests/units/Model/SubtaskTimeTrackingTest.php b/tests/units/Model/SubtaskTimeTrackingTest.php
index 9fa8d5b0..2545dcb2 100644
--- a/tests/units/Model/SubtaskTimeTrackingTest.php
+++ b/tests/units/Model/SubtaskTimeTrackingTest.php
@@ -240,81 +240,4 @@ class SubtaskTimeTrackingTest extends Base
$this->assertEquals(0, $task['time_estimated']);
$this->assertEquals(0, $task['time_spent']);
}
-
- public function testGetCalendarEvents()
- {
- $tf = new TaskFinder($this->container);
- $tc = new TaskCreation($this->container);
- $s = new Subtask($this->container);
- $st = new SubtaskTimeTracking($this->container);
- $p = new Project($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test1')));
- $this->assertEquals(2, $p->create(array('name' => 'test2')));
-
- $this->assertEquals(1, $tc->create(array('title' => 'test 1', 'project_id' => 1)));
- $this->assertEquals(2, $tc->create(array('title' => 'test 1', 'project_id' => 2)));
-
- $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1)));
- $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 1)));
- $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 1)));
-
- $this->assertEquals(4, $s->create(array('title' => 'subtask #4', 'task_id' => 2)));
- $this->assertEquals(5, $s->create(array('title' => 'subtask #5', 'task_id' => 2)));
- $this->assertEquals(6, $s->create(array('title' => 'subtask #6', 'task_id' => 2)));
- $this->assertEquals(7, $s->create(array('title' => 'subtask #7', 'task_id' => 2)));
- $this->assertEquals(8, $s->create(array('title' => 'subtask #8', 'task_id' => 2)));
-
- // Slot start before and finish inside the calendar time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 1, 'start' => strtotime('-1 day'), 'end' => strtotime('+1 hour')));
-
- // Slot start inside time range and finish after the time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 2, 'start' => strtotime('+1 hour'), 'end' => strtotime('+2 days')));
-
- // Start before time range and finish inside time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 3, 'start' => strtotime('-1 day'), 'end' => strtotime('+1.5 days')));
-
- // Start and finish inside time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 4, 'start' => strtotime('+1 hour'), 'end' => strtotime('+2 hours')));
-
- // Start and finish after the time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 5, 'start' => strtotime('+2 days'), 'end' => strtotime('+3 days')));
-
- // Start and finish before the time range
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 6, 'start' => strtotime('-2 days'), 'end' => strtotime('-1 day')));
-
- // Start before time range and not finished
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 7, 'start' => strtotime('-1 day')));
-
- // Start inside time range and not finish
- $this->container['db']->table(SubtaskTimeTracking::TABLE)->insert(array('user_id' => 1, 'subtask_id' => 8, 'start' => strtotime('+3200 seconds')));
-
- $timesheet = $st->getUserTimesheet(1);
- $this->assertNotEmpty($timesheet);
- $this->assertCount(8, $timesheet);
-
- $events = $st->getUserCalendarEvents(1, date('Y-m-d'), date('Y-m-d', strtotime('+2 day')));
- $this->assertNotEmpty($events);
- $this->assertCount(6, $events);
- $this->assertEquals(1, $events[0]['subtask_id']);
- $this->assertEquals(2, $events[1]['subtask_id']);
- $this->assertEquals(3, $events[2]['subtask_id']);
- $this->assertEquals(4, $events[3]['subtask_id']);
- $this->assertEquals(7, $events[4]['subtask_id']);
- $this->assertEquals(8, $events[5]['subtask_id']);
-
- $events = $st->getProjectCalendarEvents(1, date('Y-m-d'), date('Y-m-d', strtotime('+2 days')));
- $this->assertNotEmpty($events);
- $this->assertCount(3, $events);
- $this->assertEquals(1, $events[0]['subtask_id']);
- $this->assertEquals(2, $events[1]['subtask_id']);
- $this->assertEquals(3, $events[2]['subtask_id']);
-
- $events = $st->getProjectCalendarEvents(2, date('Y-m-d'), date('Y-m-d', strtotime('+2 days')));
- $this->assertNotEmpty($events);
- $this->assertCount(3, $events);
- $this->assertEquals(4, $events[0]['subtask_id']);
- $this->assertEquals(7, $events[1]['subtask_id']);
- $this->assertEquals(8, $events[2]['subtask_id']);
- }
}
diff --git a/tests/units/Model/TaskFilterTest.php b/tests/units/Model/TaskFilterTest.php
deleted file mode 100644
index 9e291c31..00000000
--- a/tests/units/Model/TaskFilterTest.php
+++ /dev/null
@@ -1,624 +0,0 @@
-<?php
-
-require_once __DIR__.'/../Base.php';
-
-use Kanboard\Model\Project;
-use Kanboard\Model\User;
-use Kanboard\Model\TaskFilter;
-use Kanboard\Model\TaskCreation;
-use Kanboard\Model\TaskLink;
-use Kanboard\Core\DateParser;
-use Kanboard\Model\Category;
-use Kanboard\Model\Subtask;
-use Kanboard\Model\Swimlane;
-
-class TaskFilterTest extends Base
-{
- public function testSearchWithEmptyResult()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time()))));
-
- $this->assertEmpty($tf->search('search something')->findAll());
- }
-
- public function testSearchWithEmptyInput()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time()))));
-
- $result = $tf->search('')->findAll();
- $this->assertNotEmpty($result);
- $this->assertCount(4, $result);
- }
-
- public function testSearchById()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task 43')));
-
- $tf->search('#2');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
-
- $tf->search('1');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
-
- $tf->search('something');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('#');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('#abcd');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('task1');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
-
- $tf->search('43');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task 43', $tasks[0]['title']);
- }
-
- public function testSearchWithReference()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'reference' => 123)));
-
- $tf->search('ref:123');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
-
- $tf->search('reference:123');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
-
- $tf->search('ref:plop');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('ref:');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithStatus()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'is_active' => 0)));
-
- $tf->search('status:open');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
-
- $tf->search('status:plop');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
-
- $tf->search('status:closed');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- }
-
- public function testSearchWithDescription()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'description' => '**something to do**')));
-
- $tf->search('description:"something"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
-
- $tf->search('description:"rainy day"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithCategory()
- {
- $p = new Project($this->container);
- $c = new Category($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(1, $c->create(array('name' => 'Feature request', 'project_id' => 1)));
- $this->assertEquals(2, $c->create(array('name' => 'hé hé', 'project_id' => 1)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'category_id' => 1)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task3', 'category_id' => 2)));
-
- $tf->search('category:"Feature request"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('Feature request', $tasks[0]['category_name']);
-
- $tf->search('category:"hé hé"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task3', $tasks[0]['title']);
- $this->assertEquals('hé hé', $tasks[0]['category_name']);
-
- $tf->search('category:"Feature request" category:"hé hé"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('Feature request', $tasks[0]['category_name']);
- $this->assertEquals('task3', $tasks[1]['title']);
- $this->assertEquals('hé hé', $tasks[1]['category_name']);
-
- $tf->search('category:none');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('', $tasks[0]['category_name']);
-
- $tf->search('category:"not found"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithProject()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'My project A')));
- $this->assertEquals(2, $p->create(array('name' => 'My project B')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 2, 'title' => 'task2')));
-
- $tf->search('project:"My project A"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('My project A', $tasks[0]['project_name']);
-
- $tf->search('project:2');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('My project B', $tasks[0]['project_name']);
-
- $tf->search('project:"My project A" project:"my project b"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('My project A', $tasks[0]['project_name']);
- $this->assertEquals('task2', $tasks[1]['title']);
- $this->assertEquals('My project B', $tasks[1]['project_name']);
-
- $tf->search('project:"not found"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithSwimlane()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
- $s = new Swimlane($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'My project A')));
- $this->assertEquals(1, $s->create(array('project_id' => 1, 'name' => 'Version 1.1')));
- $this->assertEquals(2, $s->create(array('project_id' => 1, 'name' => 'Version 1.2')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1', 'swimlane_id' => 1)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'swimlane_id' => 2)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task3', 'swimlane_id' => 0)));
-
- $tf->search('swimlane:"Version 1.1"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('Version 1.1', $tasks[0]['swimlane_name']);
-
- $tf->search('swimlane:"versioN 1.2"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('Version 1.2', $tasks[0]['swimlane_name']);
-
- $tf->search('swimlane:"Default swimlane"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task3', $tasks[0]['title']);
- $this->assertEquals('Default swimlane', $tasks[0]['default_swimlane']);
- $this->assertEquals('', $tasks[0]['swimlane_name']);
-
- $tf->search('swimlane:default');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task3', $tasks[0]['title']);
- $this->assertEquals('Default swimlane', $tasks[0]['default_swimlane']);
- $this->assertEquals('', $tasks[0]['swimlane_name']);
-
- $tf->search('swimlane:"Version 1.1" swimlane:"Version 1.2"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('Version 1.1', $tasks[0]['swimlane_name']);
- $this->assertEquals('task2', $tasks[1]['title']);
- $this->assertEquals('Version 1.2', $tasks[1]['swimlane_name']);
-
- $tf->search('swimlane:"not found"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithColumn()
- {
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'My project A')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task1')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'task2', 'column_id' => 3)));
-
- $tf->search('column:Backlog');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('Backlog', $tasks[0]['column_name']);
-
- $tf->search('column:backlog column:"Work in progress"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('Backlog', $tasks[0]['column_name']);
- $this->assertEquals('task2', $tasks[1]['title']);
- $this->assertEquals('Work in progress', $tasks[1]['column_name']);
-
- $tf->search('column:"not found"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
- }
-
- public function testSearchWithDueDate()
- {
- $dp = new DateParser($this->container);
- $p = new Project($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'date_due' => $dp->getTimestampFromIsoFormat('-2 days'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'date_due' => $dp->getTimestampFromIsoFormat('+1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'date_due' => $dp->getTimestampFromIsoFormat('-1 day'))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'youpi', 'date_due' => $dp->getTimestampFromIsoFormat(time()))));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'no due date')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'due date at 0', 'date_due' => 0)));
-
- $tf->search('due:>'.date('Y-m-d'));
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('due:>='.date('Y-m-d'));
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
- $this->assertEquals('youpi', $tasks[1]['title']);
-
- $tf->search('due:<'.date('Y-m-d'));
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('Bob at work', $tasks[1]['title']);
-
- $tf->search('due:<='.date('Y-m-d'));
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('Bob at work', $tasks[1]['title']);
- $this->assertEquals('youpi', $tasks[2]['title']);
-
- $tf->search('due:tomorrow');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('due:yesterday');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('Bob at work', $tasks[0]['title']);
-
- $tf->search('due:today');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('youpi', $tasks[0]['title']);
- }
-
- public function testSearchWithColor()
- {
- $p = new Project($this->container);
- $u = new User($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'color_id' => 'light_green')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'color_id' => 'blue')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work')));
-
- $tf->search('color:"Light Green"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
-
- $tf->search('color:"Light Green" amazing');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('color:"plop');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('color:unknown');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
-
- $tf->search('color:blue amazing');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('color:blue color:Yellow');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
- $this->assertEquals('Bob at work', $tasks[1]['title']);
- }
-
- public function testSearchWithAssignee()
- {
- $p = new Project($this->container);
- $u = new User($this->container);
- $tc = new TaskCreation($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'owner_id' => 1)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'owner_id' => 0)));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work', 'owner_id' => 2)));
-
- $tf->search('assignee:john');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('assignee:admin my task title');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
-
- $tf->search('my task title');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('my task title is amazing', $tasks[1]['title']);
-
- $tf->search('my task title assignee:nobody');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('assignee:"Bob ryan" assignee:nobody');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
- $this->assertEquals('Bob at work', $tasks[1]['title']);
- }
-
- public function testSearchWithAssigneeIncludingSubtasks()
- {
- $p = new Project($this->container);
- $u = new User($this->container);
- $tc = new TaskCreation($this->container);
- $s = new Subtask($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Paul Ryan')));
-
- $this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'task1', 'owner_id' => 2)));
- $this->assertEquals(1, $s->create(array('title' => 'subtask #1', 'task_id' => 1, 'status' => 1, 'user_id' => 0)));
-
- $this->assertEquals(2, $tc->create(array('project_id' => 1, 'title' => 'task2', 'owner_id' => 0)));
- $this->assertEquals(2, $s->create(array('title' => 'subtask #2', 'task_id' => 2, 'status' => 1, 'user_id' => 2)));
-
- $this->assertEquals(3, $tc->create(array('project_id' => 1, 'title' => 'task3', 'owner_id' => 0)));
- $this->assertEquals(3, $s->create(array('title' => 'subtask #3', 'task_id' => 3, 'user_id' => 1)));
-
- $tf->search('assignee:bob');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('task2', $tasks[1]['title']);
-
- $tf->search('assignee:"Paul Ryan"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task1', $tasks[0]['title']);
- $this->assertEquals('task2', $tasks[1]['title']);
-
- $tf->search('assignee:nobody');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(2, $tasks);
- $this->assertEquals('task2', $tasks[0]['title']);
- $this->assertEquals('task3', $tasks[1]['title']);
-
- $tf->search('assignee:admin');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('task3', $tasks[0]['title']);
- }
-
- public function testSearchWithLink()
- {
- $p = new Project($this->container);
- $u = new User($this->container);
- $tc = new TaskCreation($this->container);
- $tl = new TaskLink($this->container);
- $tf = new TaskFilter($this->container);
-
- $this->assertEquals(1, $p->create(array('name' => 'test')));
- $this->assertEquals(2, $u->create(array('username' => 'bob', 'name' => 'Bob Ryan')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is awesome', 'color_id' => 'light_green')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'my task title is amazing', 'color_id' => 'blue')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'Bob at work')));
- $this->assertNotFalse($tc->create(array('project_id' => 1, 'title' => 'I have a bad feeling about that')));
- $this->assertEquals(1, $tl->create(1, 2, 9)); // #1 is a milestone of #2
- $this->assertEquals(3, $tl->create(2, 1, 2)); // #2 blocks #1
- $this->assertEquals(5, $tl->create(3, 2, 2)); // #3 blocks #2
-
- $tf->search('link:"is a milestone of"');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
-
- $tf->search('link:"is a milestone of" amazing');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('link:"unknown"');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('link:unknown');
- $tasks = $tf->findAll();
- $this->assertEmpty($tasks);
-
- $tf->search('link:blocks amazing');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(1, $tasks);
- $this->assertEquals('my task title is amazing', $tasks[0]['title']);
-
- $tf->search('link:"is a milestone of" link:blocks');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('my task title is amazing', $tasks[1]['title']);
- $this->assertEquals('Bob at work', $tasks[2]['title']);
-
- $tf->search('link:"is a milestone of" link:blocks link:unknown');
- $tasks = $tf->findAll();
- $this->assertNotEmpty($tasks);
- $this->assertCount(3, $tasks);
- $this->assertEquals('my task title is awesome', $tasks[0]['title']);
- $this->assertEquals('my task title is amazing', $tasks[1]['title']);
- $this->assertEquals('Bob at work', $tasks[2]['title']);
- }
-
- public function testCopy()
- {
- $tf = new TaskFilter($this->container);
- $filter1 = $tf->create();
- $filter2 = $tf->copy();
-
- $this->assertTrue($filter1 !== $filter2);
- $this->assertTrue($filter1->query !== $filter2->query);
- $this->assertTrue($filter1->query->condition !== $filter2->query->condition);
- }
-}