Added search in activity stream

This commit is contained in:
Frederic Guillot 2016-04-10 15:18:20 -04:00
parent 2eadfb2291
commit 9f0166502b
54 changed files with 1066 additions and 110 deletions

View File

@ -3,6 +3,7 @@ Version 1.0.28 (unreleased)
New features:
* Search in activity stream
* Search in comments
* Search by task creator

View File

@ -46,4 +46,22 @@ class Search extends Base
'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '')
)));
}
public function activity()
{
$search = urldecode($this->request->getStringParam('search'));
$events = $this->helper->projectActivity->searchEvents($search);
$nb_events = count($events);
$this->response->html($this->helper->layout->app('search/activity', array(
'values' => array(
'search' => $search,
'controller' => 'search',
'action' => 'activity',
),
'title' => t('Search in activity stream').($nb_events > 0 ? ' ('.$nb_events.')' : ''),
'nb_events' => $nb_events,
'events' => $events,
)));
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\DateParser;
/**
* Base date filter class
*
* @package filter
* @author Frederic Guillot
*/
abstract class BaseDateFilter extends BaseFilter
{
/**
* DateParser object
*
* @access protected
* @var DateParser
*/
protected $dateParser;
/**
* Set DateParser object
*
* @access public
* @param DateParser $dateParser
* @return $this
*/
public function setDateParser(DateParser $dateParser)
{
$this->dateParser = $dateParser;
return $this;
}
/**
* Parse operator in the input string
*
* @access protected
* @return string
*/
protected function parseOperator()
{
$operators = array(
'<=' => 'lte',
'>=' => 'gte',
'<' => 'lt',
'>' => 'gt',
);
foreach ($operators as $operator => $method) {
if (strpos($this->value, $operator) === 0) {
$this->value = substr($this->value, strlen($operator));
return $method;
}
}
return '';
}
/**
* Apply a date filter
*
* @access protected
* @param string $field
*/
protected function applyDateFilter($field)
{
$method = $this->parseOperator();
$timestamp = $this->dateParser->getTimestampFromIsoFormat($this->value);
if ($method !== '') {
$this->query->$method($field, $this->getTimestampFromOperator($method, $timestamp));
} else {
$this->query->gte($field, $timestamp);
$this->query->lte($field, $timestamp + 86399);
}
}
/**
* Get timestamp from the operator
*
* @access public
* @param string $method
* @param integer $timestamp
* @return integer
*/
protected function getTimestampFromOperator($method, $timestamp)
{
switch ($method) {
case 'lte':
return $timestamp + 86399;
case 'lt':
return $timestamp;
case 'gte':
return $timestamp;
case 'gt':
return $timestamp + 86400;
}
return $timestamp;
}
}

View File

@ -72,48 +72,4 @@ abstract class BaseFilter
$this->value = $value;
return $this;
}
/**
* Parse operator in the input string
*
* @access protected
* @return string
*/
protected function parseOperator()
{
$operators = array(
'<=' => 'lte',
'>=' => 'gte',
'<' => 'lt',
'>' => 'gt',
);
foreach ($operators as $operator => $method) {
if (strpos($this->value, $operator) === 0) {
$this->value = substr($this->value, strlen($operator));
return $method;
}
}
return '';
}
/**
* Apply a date filter
*
* @access protected
* @param string $field
*/
protected function applyDateFilter($field)
{
$timestamp = strtotime($this->value);
$method = $this->parseOperator();
if ($method !== '') {
$this->query->$method($field, $timestamp);
} else {
$this->query->gte($field, $timestamp);
$this->query->lte($field, $timestamp + 86399);
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectActivity;
/**
* Filter activity events by creation date
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityCreationDateFilter extends BaseDateFilter 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(ProjectActivity::TABLE.'.date_creation');
return $this;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\ProjectActivity;
/**
* Filter activity events by creator
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityCreatorFilter 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('creator');
}
/**
* Apply filter
*
* @access public
* @return string
*/
public function apply()
{
if ($this->value === 'me') {
$this->query->eq(ProjectActivity::TABLE . '.creator_id', $this->currentUserId);
} else {
$this->query->beginOr();
$this->query->ilike('uc.username', '%'.$this->value.'%');
$this->query->ilike('uc.name', '%'.$this->value.'%');
$this->query->closeOr();
}
}
}

View File

@ -21,7 +21,7 @@ class ProjectActivityProjectIdsFilter extends BaseFilter implements FilterInterf
*/
public function getAttributes()
{
return array('project_ids');
return array('projects');
}
/**

View File

@ -0,0 +1,38 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Project;
/**
* Filter activity events by project name
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityProjectNameFilter 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()
{
$this->query->ilike(Project::TABLE.'.name', '%'.$this->value.'%');
return $this;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter activity events by task status
*
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityTaskStatusFilter 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->query->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN);
} elseif ($this->value === 'closed') {
$this->query->eq(Task::TABLE.'.is_active', Task::STATUS_CLOSED);
}
return $this;
}
}

View File

@ -3,7 +3,6 @@
namespace Kanboard\Filter;
use Kanboard\Core\Filter\FilterInterface;
use Kanboard\Model\Task;
/**
* Filter activity events by task title
@ -11,7 +10,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
class ProjectActivityTaskTitleFilter extends BaseFilter implements FilterInterface
class ProjectActivityTaskTitleFilter extends TaskTitleFilter implements FilterInterface
{
/**
* Get search attribute
@ -23,16 +22,4 @@ class ProjectActivityTaskTitleFilter extends BaseFilter implements FilterInterfa
{
return array('title');
}
/**
* Apply filter
*
* @access public
* @return FilterInterface
*/
public function apply()
{
$this->query->ilike(Task::TABLE.'.title', '%'.$this->value.'%');
return $this;
}
}

View File

@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
class TaskCompletionDateFilter extends BaseFilter implements FilterInterface
class TaskCompletionDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute

View File

@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
class TaskCreationDateFilter extends BaseFilter implements FilterInterface
class TaskCreationDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute

View File

@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
class TaskDueDateFilter extends BaseFilter implements FilterInterface
class TaskDueDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute

View File

@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
class TaskModificationDateFilter extends BaseFilter implements FilterInterface
class TaskModificationDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute

View File

@ -32,7 +32,12 @@ class TaskProjectsFilter extends BaseFilter implements FilterInterface
*/
public function apply()
{
$this->query->in(Task::TABLE.'.project_id', $this->value);
if (empty($this->value)) {
$this->query->eq(Task::TABLE.'.project_id', 0);
} else {
$this->query->in(Task::TABLE.'.project_id', $this->value);
}
return $this;
}
}

View File

@ -11,7 +11,7 @@ use Kanboard\Model\Task;
* @package filter
* @author Frederic Guillot
*/
class TaskStartDateFilter extends BaseFilter implements FilterInterface
class TaskStartDateFilter extends BaseDateFilter implements FilterInterface
{
/**
* Get search attribute

View File

@ -17,6 +17,33 @@ use Kanboard\Model\ProjectActivity;
*/
class ProjectActivityHelper extends Base
{
/**
* Search events
*
* @access public
* @param string $search
* @return array
*/
public function searchEvents($search)
{
$projects = $this->projectUserRole->getProjectsByUser($this->userSession->getId());
$events = array();
if ($search !== '') {
$queryBuilder = $this->projectActivityLexer->build($search);
$queryBuilder
->withFilter(new ProjectActivityProjectIdsFilter(array_keys($projects)))
->getQuery()
->desc(ProjectActivity::TABLE.'.id')
->limit(500)
;
$events = $queryBuilder->format(new ProjectActivityEventFormatter($this->container));
}
return $events;
}
/**
* Get project activity events
*

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
'Upload my avatar image' => 'Mein Avatar Bild hochladen',
'Remove my image' => 'Mein Bild entfernen',
'The OAuth2 state parameter is invalid' => 'Der OAuth2 Statusparameter ist ungültig',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
'Upload my avatar image' => 'Uploader mon image d\'avatar',
'Remove my image' => 'Supprimer mon image',
'The OAuth2 state parameter is invalid' => 'Le paramètre "state" de OAuth2 est invalide',
'User not found.' => 'Utilisateur introuvable.',
'Search in activity stream' => 'Chercher dans le flux d\'activité',
'My activities' => 'Mes activités',
'Activity until yesterday' => 'Activités jusqu\'à hier',
'Activity until today' => 'Activités jusqu\'à aujourd\'hui',
'Search by creator: ' => 'Rechercher par créateur : ',
'Search by creation date: ' => 'Rechercher par date de création : ',
'Search by task status: ' => 'Rechercher par le statut des tâches : ',
'Search by task title: ' => 'Rechercher par le titre des tâches : ',
'Activity stream search' => 'Recherche dans le flux d\'activité',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
'Upload my avatar image' => 'Enviar a minha imagem de avatar',
'Remove my image' => 'Remover a minha imagem',
'The OAuth2 state parameter is invalid' => 'O parâmetro "state" de OAuth2 não é válido',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
'Upload my avatar image' => 'Enviar a minha imagem de avatar',
'Remove my image' => 'Remover a minha imagem',
'The OAuth2 state parameter is invalid' => 'O parametro de estado do OAuth2 é inválido',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1152,5 +1152,15 @@ return array(
'Avatar' => 'Аватар',
'Upload my avatar image' => 'Загрузить моё изображение для аватара',
'Remove my image' => 'Удалить моё изображение',
'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный'
'The OAuth2 state parameter is invalid' => 'Параметр состояние OAuth2 неправильный',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -1153,4 +1153,14 @@ return array(
// 'Upload my avatar image' => '',
// 'Remove my image' => '',
// 'The OAuth2 state parameter is invalid' => '',
// 'User not found.' => '',
// 'Search in activity stream' => '',
// 'My activities' => '',
// 'Activity until yesterday' => '',
// 'Activity until today' => '',
// 'Search by creator: ' => '',
// 'Search by creation date: ' => '',
// 'Search by task status: ' => '',
// 'Search by task title: ' => '',
// 'Activity stream search' => '',
);

View File

@ -71,6 +71,7 @@ class ProjectActivity extends Base
'uc.avatar_path'
)
->join(Task::TABLE, 'id', 'task_id')
->join(Project::TABLE, 'id', 'project_id')
->left(User::TABLE, 'uc', 'id', ProjectActivity::TABLE, 'creator_id');
}

View File

@ -4,6 +4,10 @@ namespace Kanboard\ServiceProvider;
use Kanboard\Core\Filter\LexerBuilder;
use Kanboard\Core\Filter\QueryBuilder;
use Kanboard\Filter\ProjectActivityCreationDateFilter;
use Kanboard\Filter\ProjectActivityCreatorFilter;
use Kanboard\Filter\ProjectActivityProjectNameFilter;
use Kanboard\Filter\ProjectActivityTaskStatusFilter;
use Kanboard\Filter\ProjectActivityTaskTitleFilter;
use Kanboard\Filter\TaskAssigneeFilter;
use Kanboard\Filter\TaskCategoryFilter;
@ -86,8 +90,18 @@ class FilterProvider implements ServiceProviderInterface
$container['projectActivityLexer'] = $container->factory(function ($c) {
$builder = new LexerBuilder();
$builder->withQuery($c['projectActivity']->getQuery());
$builder->withFilter(new ProjectActivityTaskTitleFilter());
$builder
->withQuery($c['projectActivity']->getQuery())
->withFilter(new ProjectActivityTaskTitleFilter(), true)
->withFilter(new ProjectActivityTaskStatusFilter())
->withFilter(new ProjectActivityProjectNameFilter())
->withFilter(ProjectActivityCreationDateFilter::getInstance()
->setDateParser($c['dateParser'])
)
->withFilter(ProjectActivityCreatorFilter::getInstance()
->setCurrentUserId($c['userSession']->getId())
)
;
return $builder;
});
@ -124,17 +138,23 @@ class FilterProvider implements ServiceProviderInterface
)
->withFilter(new TaskColumnFilter())
->withFilter(new TaskCommentFilter())
->withFilter(new TaskCreationDateFilter())
->withFilter(TaskCreationDateFilter::getInstance()
->setDateParser($c['dateParser'])
)
->withFilter(TaskCreatorFilter::getInstance()
->setCurrentUserId($c['userSession']->getId())
)
->withFilter(new TaskDescriptionFilter())
->withFilter(new TaskDueDateFilter())
->withFilter(TaskDueDateFilter::getInstance()
->setDateParser($c['dateParser'])
)
->withFilter(new TaskIdFilter())
->withFilter(TaskLinkFilter::getInstance()
->setDatabase($c['db'])
)
->withFilter(new TaskModificationDateFilter())
->withFilter(TaskModificationDateFilter::getInstance()
->setDateParser($c['dateParser'])
)
->withFilter(new TaskProjectFilter())
->withFilter(new TaskReferenceFilter())
->withFilter(new TaskStatusFilter())

View File

@ -42,7 +42,7 @@ class RouteProvider implements ServiceProviderInterface
// Search routes
$container['route']->addRoute('search', 'search', 'index');
$container['route']->addRoute('search/:search', 'search', 'index');
$container['route']->addRoute('search/activity', 'search', 'activity');
// ProjectCreation routes
$container['route']->addRoute('project/create', 'ProjectCreation', 'create');
@ -62,6 +62,7 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('project/:project_id/enable', 'project', 'enable');
$container['route']->addRoute('project/:project_id/permissions', 'ProjectPermission', 'index');
$container['route']->addRoute('project/:project_id/import', 'taskImport', 'step1');
$container['route']->addRoute('project/:project_id/activity', 'activity', 'project');
// Project Overview
$container['route']->addRoute('project/:project_id/overview', 'ProjectOverview', 'show');

View File

@ -0,0 +1,14 @@
<div class="dropdown">
<a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Default filters') ?>"><i class="fa fa-filter fa-fw"></i><i class="fa fa-caret-down"></i></a>
<ul>
<li><a href="#" class="filter-helper filter-reset" data-filter="" title="<?= t('Keyboard shortcut: "%s"', 'r') ?>"><?= t('Reset filters') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="creator:me"><?= t('My activities') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d', strtotime('yesterday')) ?>"><?= t('Activity until yesterday') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="created:<=<?= date('Y-m-d')?>"><?= t('Activity until today') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:closed"><?= t('Closed tasks') ?></a></li>
<li><a href="#" class="filter-helper" data-filter="status:open"><?= t('Open tasks') ?></a></li>
<li>
<?= $this->url->doc(t('View advanced search syntax'), 'search') ?>
</li>
</ul>
</div>

View File

@ -0,0 +1,39 @@
<section id="main">
<div class="page-header">
<ul>
<li>
<i class="fa fa-search fa-fw"></i>
<?= $this->url->link(t('Search tasks'), 'search', 'index') ?>
</li>
</ul>
</div>
<div class="filter-box">
<form method="get" action="<?= $this->url->dir() ?>" class="search">
<?= $this->form->hidden('controller', $values) ?>
<?= $this->form->hidden('action', $values) ?>
<?= $this->form->text('search', $values, array(), array(empty($values['search']) ? 'autofocus' : '', 'placeholder="'.t('Search').'"'), 'form-input-large') ?>
<?= $this->render('activity/filter_dropdown') ?>
</form>
</div>
<?php if (empty($values['search'])): ?>
<div class="listing">
<h3><?= t('Advanced search') ?></h3>
<p><?= t('Example of query: ') ?><strong>project:"My project" creator:me</strong></p>
<ul>
<li><?= t('Search by project: ') ?><strong>project:"My project"</strong></li>
<li><?= t('Search by creator: ') ?><strong>creator:admin</strong></li>
<li><?= t('Search by creation date: ') ?><strong>created:today</strong></li>
<li><?= t('Search by task status: ') ?><strong>status:open</strong></li>
<li><?= t('Search by task title: ') ?><strong>title:"My task"</strong></li>
</ul>
<p><i class="fa fa-external-link fa-fw"></i><?= $this->url->doc(t('View advanced search syntax'), 'search') ?></p>
</div>
<?php elseif (! empty($values['search']) && $nb_events === 0): ?>
<p class="alert"><?= t('Nothing found.') ?></p>
<?php else: ?>
<?= $this->render('event/events', array('events' => $events)) ?>
<?php endif ?>
</section>

View File

@ -2,8 +2,8 @@
<div class="page-header">
<ul>
<li>
<i class="fa fa-folder fa-fw"></i>
<?= $this->url->link(t('All projects'), 'project', 'index') ?>
<i class="fa fa-search fa-fw"></i>
<?= $this->url->link(t('Activity stream search'), 'search', 'activity') ?>
</li>
</ul>
</div>

View File

@ -1,7 +1,8 @@
Advanced Search Syntax
======================
Kanboard uses a simple query language for advanced search.
Kanboard uses a simple query language for advanced search.
You can search in tasks, comments, subtasks, links but also in the activity stream.
Example of query
----------------
@ -12,23 +13,23 @@ This example will return all tasks assigned to me with a due date for tomorrow a
assigne:me due:tomorrow my title
```
Search by task id or title
--------------------------
Global search
-------------
### Search by task id or title
- Search by task id: `#123`
- Search by task id and task title: `123`
- Search by task title: anything that doesn't match any search attributes
Search by status
----------------
### Search by status
Attribute: **status**
- Query to find open tasks: `status:open`
- Query to find closed tasks: `status:closed`
Search by assignee
------------------
### Search by assignee
Attribute: **assignee**
@ -38,8 +39,7 @@ Attribute: **assignee**
- Query for unassigned tasks: `assignee:nobody`
- Query for my assigned tasks: `assignee:me`
Search by task creator
----------------------
### Search by task creator
Attribute: **creator**
@ -47,23 +47,20 @@ Attribute: **creator**
- Tasks created by John Doe: `creator:"John Doe"`
- Tasks created by the user id #1: `creator:1`
Search by subtask assignee
--------------------------
### Search by subtask assignee
Attribute: **subtask:assignee**
- Example: `subtask:assignee:"John Doe"`
Search by color
---------------
### Search by color
Attribute: **color**
- Query to search by color id: `color:blue`
- Query to search by color name: `color:"Deep Orange"`
Search by the due date
----------------------
### Search by the due date
Attribute: **due**
@ -83,8 +80,7 @@ Operators supported with a date:
- Greater than or equal: **due:>=2015-06-29**
- Lower than or equal: **due:<=2015-06-29**
Search by modification date
---------------------------
### Search by modification date
Attribute: **modified** or **updated**
@ -94,29 +90,25 @@ There is also a filter by recently modified tasks: `modified:recently`.
This query will use the same value as the board highlight period configured in settings.
Search by creation date
-----------------------
### Search by creation date
Attribute: **created**
Works in the same way as the modification date queries.
Search by description
---------------------
### Search by description
Attribute: **description** or **desc**
Example: `description:"text search"`
Search by external reference
----------------------------
### Search by external reference
The task reference is an external id of your task, by example a ticket number from another software.
- Find tasks with a reference: `ref:1234` or `reference:TICKET-1234`
Search by category
------------------
### Search by category
Attribute: **category**
@ -124,8 +116,7 @@ Attribute: **category**
- Find all tasks that have those categories: `category:"Bug" category:"Improvements"`
- Find tasks with no category assigned: `category:none`
Search by project
-----------------
### Search by project
Attribute: **project**
@ -133,16 +124,14 @@ Attribute: **project**
- Find tasks by project id: `project:23`
- Find tasks for several projects: `project:"My project A" project:"My project B"`
Search by columns
-----------------
### Search by columns
Attribute: **column**
- Find tasks by column name: `column:"Work in progress"`
- Find tasks for several columns: `column:"Backlog" column:ready`
Search by swim-lane
-------------------
### Search by swim-lane
Attribute: **swimlane**
@ -150,17 +139,41 @@ Attribute: **swimlane**
- Find tasks in the default swim-lane: `swimlane:default`
- Find tasks into several swim-lanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"`
Search by task link
------------------
### Search by task link
Attribute: **link**
- Find tasks by link name: `link:"is a milestone of"`
- Find tasks into several links: `link:"is a milestone of" link:"relates to"`
Search by comment
-----------------
### Search by comment
Attribute: **comment**
- Find comments that contains this title: `comment:"My comment message"`
Activity stream search
----------------------
### Search events by task title
Attribute: **title** or none (default)
- Example: `title:"My task"`
- Search by task id: `#123`
### Search events by task status
Attribute: **status**
### Search by event creator
Attribute: **creator**
### Search by event creation date
Attribute: **created**
### Search events by project
Attribute: **project**

View File

@ -0,0 +1,117 @@
<?php
use Kanboard\Filter\ProjectActivityCreationDateFilter;
use Kanboard\Model\Project;
use Kanboard\Model\ProjectActivity;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Task;
require_once __DIR__.'/../Base.php';
class ProjectActivityCreationDateFilterTest extends Base
{
public function testWithToday()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreationDateFilter('today');
$filter->setDateParser($this->container['dateParser']);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
}
public function testWithYesterday()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreationDateFilter('yesterday');
$filter->setDateParser($this->container['dateParser']);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(0, $events);
}
public function testWithIsoDate()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreationDateFilter(date('Y-m-d'));
$filter->setDateParser($this->container['dateParser']);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
}
public function testWithOperatorAndIsoDate()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreationDateFilter('>='.date('Y-m-d'));
$filter->setDateParser($this->container['dateParser']);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreationDateFilter('<'.date('Y-m-d'));
$filter->setDateParser($this->container['dateParser']);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(0, $events);
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreationDateFilter('>'.date('Y-m-d'));
$filter->setDateParser($this->container['dateParser']);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(0, $events);
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreationDateFilter('>='.date('Y-m-d'));
$filter->setDateParser($this->container['dateParser']);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
}
}

View File

@ -0,0 +1,91 @@
<?php
use Kanboard\Filter\ProjectActivityCreatorFilter;
use Kanboard\Model\Project;
use Kanboard\Model\ProjectActivity;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Task;
require_once __DIR__.'/../Base.php';
class ProjectActivityCreatorFilterTest extends Base
{
public function testWithUsername()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreatorFilter('admin');
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
}
public function testWithAnotherUsername()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreatorFilter('John Doe');
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(0, $events);
}
public function testWithCurrentUser()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreatorFilter('me');
$filter->setCurrentUserId(1);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
}
public function testWithAnotherCurrentUser()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityCreatorFilter('me');
$filter->setCurrentUserId(2);
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(0, $events);
}
}

View File

@ -0,0 +1,35 @@
<?php
use Kanboard\Filter\ProjectActivityProjectNameFilter;
use Kanboard\Model\Project;
use Kanboard\Model\ProjectActivity;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Task;
require_once __DIR__.'/../Base.php';
class ProjectActivityProjectNameFilterTest extends Base
{
public function testFilterByProjectName()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$query = $projectActivityModel->getQuery();
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(2, $projectModel->create(array('name' => 'P2')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 2)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$this->assertNotFalse($projectActivityModel->createEvent(2, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
$filter = new ProjectActivityProjectNameFilter('P1');
$filter->withQuery($query)->apply();
$this->assertCount(1, $query->findAll());
}
}

View File

@ -0,0 +1,49 @@
<?php
use Kanboard\Filter\ProjectActivityTaskStatusFilter;
use Kanboard\Model\Project;
use Kanboard\Model\ProjectActivity;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Task;
use Kanboard\Model\TaskStatus;
require_once __DIR__.'/../Base.php';
class ProjectActivityTaskStatusFilterTest extends Base
{
public function testFilterByTaskStatus()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$taskStatus = new TaskStatus($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertEquals(2, $taskCreation->create(array('title' => 'Test', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
$this->assertTrue($taskStatus->close(1));
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityTaskStatusFilter('open');
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
$this->assertEquals(2, $events[0]['task_id']);
$query = $projectActivityModel->getQuery();
$filter = new ProjectActivityTaskStatusFilter('closed');
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
$this->assertEquals(1, $events[0]['task_id']);
}
}

View File

@ -11,7 +11,7 @@ require_once __DIR__.'/../Base.php';
class ProjectActivityTaskTitleFilterTest extends Base
{
public function testFilterByTaskId()
public function testWithFullTitle()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
@ -31,4 +31,49 @@ class ProjectActivityTaskTitleFilterTest extends Base
$filter->withQuery($query)->apply();
$this->assertCount(1, $query->findAll());
}
public function testWithPartialTitle()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$query = $projectActivityModel->getQuery();
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1)));
$this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
$filter = new ProjectActivityTaskTitleFilter('test');
$filter->withQuery($query)->apply();
$this->assertCount(2, $query->findAll());
}
public function testWithId()
{
$taskFinder = new TaskFinder($this->container);
$taskCreation = new TaskCreation($this->container);
$projectModel = new Project($this->container);
$projectActivityModel = new ProjectActivity($this->container);
$query = $projectActivityModel->getQuery();
$this->assertEquals(1, $projectModel->create(array('name' => 'P1')));
$this->assertEquals(1, $taskCreation->create(array('title' => 'Test1', 'project_id' => 1)));
$this->assertEquals(2, $taskCreation->create(array('title' => 'Test2', 'project_id' => 1)));
$this->assertNotFalse($projectActivityModel->createEvent(1, 1, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(1))));
$this->assertNotFalse($projectActivityModel->createEvent(1, 2, 1, Task::EVENT_CREATE, array('task' => $taskFinder->getById(2))));
$filter = new ProjectActivityTaskTitleFilter('#2');
$filter->withQuery($query)->apply();
$events = $query->findAll();
$this->assertCount(1, $events);
$this->assertEquals(2, $events[0]['task_id']);
}
}