Filter refactoring

This commit is contained in:
Frederic Guillot 2016-04-09 22:42:17 -04:00
parent 42813d702d
commit 11858be4e8
101 changed files with 3235 additions and 2841 deletions

View File

@ -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

View File

@ -1,3 +1,10 @@
Version 1.0.28 (unreleased)
--------------
Improvements:
* Filter/Lexer/QueryBuilder refactoring
Version 1.0.27
--------------

View File

@ -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(

View File

@ -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))
));
}
}

View File

@ -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();
$events = $this->helper->calendar->getTaskDateDueEvents(clone($queryBuilder), $start, $end);
$events = array_merge($events, $this->helper->calendar->getTaskEvents(clone($queryBuilder), $start, $end));
// 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());
}
// 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(

View File

@ -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)),
)));
}

View File

@ -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());
}
}

View File

@ -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);
}
$this->helper->ical->addTaskDateDueEvents($queryBuilder, $calendar, $start, $end);
// Tasks with due date
$filter
->copy()
->filterByDueDateRange($start, $end)
->setColumns('date_due')
->setCalendar($calendar)
->addFullDayEvents($calendar);
$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());
}
}

View File

@ -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(

View File

@ -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();

View File

@ -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());
}
$filter = $this->taskFilterAutoCompleteFormatter
->create()
->filterByProjects($projects)
->excludeTasks(array($this->request->getIntegerParam('exclude_task_id')));
// Search by task id or by title
if (ctype_digit($search)) {
$filter->filterById($search);
} else {
$filter->filterByTitle($search);
}
$this->response->json($filter->format());
$filter = $this->taskQuery->withFilter(new TaskProjectsFilter($project_ids));
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(new TaskAutoCompleteFormatter($this->container)));
}
}
}

View File

@ -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
*/

View File

@ -18,7 +18,7 @@ class ActionManager extends Base
* List of automatic actions
*
* @access private
* @var array
* @var ActionBase[]
*/
private $actions = array();

View File

@ -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;
}
}

View File

@ -23,7 +23,7 @@ class ExternalLinkManager extends Base
* Registered providers
*
* @access private
* @var array
* @var ExternalLinkProviderInterface[]
*/
private $providers = array();

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

153
app/Core/Filter/Lexer.php Normal file
View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -231,6 +231,20 @@ class Response extends Base
exit;
}
/**
* Send a iCal response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
*/
public function ical($data, $status_code = 200)
{
$this->status($status_code);
$this->contentType('text/calendar; charset=utf-8');
echo $data;
}
/**
* Send the security header: Content-Security-Policy
*

View File

@ -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;
}
}

119
app/Filter/BaseFilter.php Normal file
View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
@ -28,21 +28,13 @@ 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;
}
}

View File

@ -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;
});
});
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace Kanboard\Formatter;
/**
* Formatter Interface
*
* @package formatter
* @author Frederic Guillot
*/
interface FormatterInterface
{
public function format();
}

View File

@ -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

View File

@ -2,7 +2,7 @@
namespace Kanboard\Formatter;
use Kanboard\Model\Project;
use Kanboard\Core\Filter\FormatterInterface;
/**
* Gantt chart formatter for projects
@ -10,40 +10,8 @@ use Kanboard\Model\Project;
* @package formatter
* @author Frederic Guillot
*/
class ProjectGanttFormatter extends Project implements FormatterInterface
class ProjectGanttFormatter extends BaseFormatter implements FormatterInterface
{
/**
* List of projects
*
* @access private
* @var array
*/
private $projects = array();
/**
* Filter projects to generate the Gantt chart
*
* @access public
* @param int[] $project_ids
* @return ProjectGanttFormatter
*/
public function filter(array $project_ids)
{
if (empty($project_ids)) {
$this->projects = array();
} else {
$this->projects = $this->db
->table(self::TABLE)
->asc('start_date')
->in('id', $project_ids)
->eq('is_active', self::ACTIVE)
->eq('is_private', 0)
->findAll();
}
return $this;
}
/**
* Format projects to be displayed in the Gantt chart
*
@ -52,10 +20,11 @@ class ProjectGanttFormatter extends Project implements FormatterInterface
*/
public function format()
{
$projects = $this->query->findAll();
$colors = $this->color->getDefaultColors();
$bars = array();
foreach ($this->projects as $project) {
foreach ($projects as $project) {
$start = empty($project['start_date']) ? time() : strtotime($project['start_date']);
$end = empty($project['end_date']) ? $start : strtotime($project['end_date']);
$color = next($colors) ?: reset($colors);

View File

@ -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;
}
}

View File

@ -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

View File

@ -2,14 +2,36 @@
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
*
@ -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';
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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).')';
}
}

38
app/Helper/ICalHelper.php Normal file
View File

@ -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();
}
}

View File

@ -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)
{

View File

@ -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).')';
}
}

View File

@ -34,6 +34,20 @@ class Project extends Base
*/
const INACTIVE = 0;
/**
* Value for private project
*
* @var integer
*/
const TYPE_PRIVATE = 1;
/**
* Value for team project
*
* @var integer
*/
const TYPE_TEAM = 0;
/**
* Get a project by the id
*

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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)

View File

@ -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;
}
}

View File

@ -22,6 +22,7 @@ abstract class Setting extends Base
*
* @abstract
* @access public
* @param array $values
* @return array
*/
abstract public function prepare(array $values);

View File

@ -145,94 +145,6 @@ class SubtaskTimeTracking extends Base
->findAll();
}
/**
* Get user calendar events
*
* @access public
* @param integer $user_id
* @param string $start ISO-8601 format
* @param string $end
* @return array
*/
public function getUserCalendarEvents($user_id, $start, $end)
{
$hook = 'model:subtask-time-tracking:calendar:events';
$events = $this->getUserQuery($user_id)
->addCondition($this->getCalendarCondition(
$this->dateParser->getTimestampFromIsoFormat($start),
$this->dateParser->getTimestampFromIsoFormat($end),
'start',
'end'
))
->findAll();
if ($this->hook->exists($hook)) {
$events = $this->hook->first($hook, array(
'user_id' => $user_id,
'events' => $events,
'start' => $start,
'end' => $end,
));
}
return $this->toCalendarEvents($events);
}
/**
* Get project calendar events
*
* @access public
* @param integer $project_id
* @param integer $start
* @param integer $end
* @return array
*/
public function getProjectCalendarEvents($project_id, $start, $end)
{
$result = $this
->getProjectQuery($project_id)
->addCondition($this->getCalendarCondition(
$this->dateParser->getTimestampFromIsoFormat($start),
$this->dateParser->getTimestampFromIsoFormat($end),
'start',
'end'
))
->findAll();
return $this->toCalendarEvents($result);
}
/**
* Convert a record set to calendar events
*
* @access private
* @param array $rows
* @return array
*/
private function toCalendarEvents(array $rows)
{
$events = array();
foreach ($rows as $row) {
$user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : '';
$events[] = array(
'id' => $row['id'],
'subtask_id' => $row['subtask_id'],
'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user,
'start' => date('Y-m-d\TH:i:s', $row['start']),
'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()),
'backgroundColor' => $this->color->getBackgroundColor($row['color_id']),
'borderColor' => $this->color->getBorderColor($row['color_id']),
'textColor' => 'black',
'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])),
'editable' => false,
);
}
return $events;
}
/**
* Return true if a timer is started for this use and subtask
*

View File

@ -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;
}
}

View File

@ -362,6 +362,27 @@ class TaskFinder extends Base
return $rq->fetch(PDO::FETCH_ASSOC);
}
/**
* Get iCal query
*
* @access public
* @return \PicoDb\Table
*/
public function getICalQuery()
{
return $this->db->table(Task::TABLE)
->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id')
->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id')
->columns(
Task::TABLE.'.*',
'ua.email AS assignee_email',
'ua.name AS assignee_name',
'ua.username AS assignee_username',
'uc.email AS creator_email',
'uc.username AS creator_username'
);
}
/**
* Count all tasks for a given project and status
*

View File

@ -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;
}
}

View File

@ -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',

View File

@ -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;
}
}

View File

@ -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');

View File

@ -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);

20
composer.lock generated
View File

@ -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",

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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());
}
}

View File

@ -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'));
}
}

View File

@ -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());
}
}

View File

@ -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:'))
);
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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']);
}
}

Some files were not shown because too many files have changed in this diff Show More